77 Commits

Author SHA1 Message Date
putyy
046cbb2b83 fix: proxy 2025-12-31 10:24:50 +08:00
putyy
b562f76c69 feat: add url search, update version 2025-12-30 23:47:12 +08:00
putyy
8aaf95fd36 perf: domain rule 2025-12-30 23:47:12 +08:00
putyy
983d72d65a feat: add domain rule configuration 2025-12-30 23:47:12 +08:00
putyy
86378b9fba perf: domain rule 2025-12-27 11:32:00 +08:00
putyy
6b18e7fba1 feat: add domain rule configuration 2025-12-26 17:20:05 +08:00
putyy
ec11132240 perf: optimization type support 2025-12-26 17:20:05 +08:00
putyy
dc877bd634 perf: change icon color when filtering description field 2025-11-30 12:53:38 +08:00
putyy
00b4bf4068 feat: editable description field 2025-11-30 12:53:38 +08:00
putyy
51c43564b6 perf: save ongoing tasks when deleting records 2025-11-30 12:53:38 +08:00
putyy
820a2671cf fix: batch cancel 2025-10-16 12:24:40 +08:00
putyy
3b4443110e feat: update version 2025-10-15 21:07:00 +08:00
putyy
ffd5b29030 feat: add size sorting、remember clear list choice、reset App,optimize preview 2025-10-15 21:03:20 +08:00
putyy
779f56dd91 fix: batch download 2025-09-23 09:25:36 +08:00
putyy
2beecdade2 fix: windows file naming during download 2025-09-16 10:12:54 +08:00
putyy
bca2e110de feat: update version 2025-09-14 21:56:30 +08:00
putyy
8d55a86c06 feat: add loading check 2025-09-14 21:45:48 +08:00
putyy
f61199bed6 feat: add batch cancel, batch export link 2025-09-14 16:14:52 +08:00
putyy
2d75bbb5c3 feat: add cancel download 2025-09-13 22:25:14 +08:00
putyy
55d3f06cb6 perf: optimize file naming during download 2025-09-12 10:06:05 +08:00
putyy
1809847b8a perf: optimize file naming during download 2025-09-11 17:04:07 +08:00
putyy
da8e8d9641 perf: downloader cancel timeout 2025-09-11 17:04:07 +08:00
putyy
ead622d95e fix: qq plugin optimize 2025-09-11 17:04:07 +08:00
putyy
c47fcba36b fix: filter classify 2025-09-11 17:04:07 +08:00
putyy
54c0da081c fix: linux build 2025-07-29 17:40:16 +08:00
putyy
4bead0752d Merge branch 'master' into wails 2025-07-27 16:29:21 +08:00
putyy
bd7828b73f fix: version comparison 2025-07-27 16:26:40 +08:00
putyy
4706540475 docs: update document 2025-07-25 16:10:41 +08:00
putyy
0daec66fa6 docs: update document 2025-07-25 16:02:27 +08:00
putyy
379ae22db7 feat: update version to 3.1.0 2025-07-25 15:43:22 +08:00
putyy
2d1fc4273a feat: picture display optimize 2025-07-25 15:43:22 +08:00
putyy
55b67a0efa feat: insert mode setting 2025-07-25 15:43:22 +08:00
putyy
ace4625a27 perf: index page optimization 2025-07-25 15:43:22 +08:00
putyy
6fb0474154 doc: delete dartNode 2025-07-25 15:43:22 +08:00
putyy
86ef0d3331 perf: downloader optimization 2025-07-25 15:43:22 +08:00
putyy
cfa9d4929f fix: download action 2025-07-25 15:43:22 +08:00
putyy
3c40ada451 perf: Batch export、operation item style optimization 2025-07-25 15:43:22 +08:00
putyy
9ec4eca558 perf: setting optimization 2025-07-25 15:43:22 +08:00
putyy
f295fb6b64 perf: index height calculation optimization 2025-07-25 15:43:22 +08:00
putyy
3dc4322258 feat: update version to 3.1.0 2025-07-25 15:42:51 +08:00
putyy
3910d0ffb0 feat: picture display optimize 2025-07-25 15:37:10 +08:00
putyy
af75f1ce4f feat: insert mode setting 2025-07-25 11:46:07 +08:00
putyy
a016465bea Merge branch 'master' into wails 2025-07-25 10:32:33 +08:00
putyy
fd5e289c87 perf: index page optimization 2025-07-25 10:31:46 +08:00
putyy
2a2ca7eb4e doc: delete dartNode 2025-07-24 17:24:04 +08:00
putyy
a7ec61b8e2 perf: downloader optimization 2025-07-24 17:16:30 +08:00
putyy
84c882d573 fix: download action 2025-07-23 16:11:44 +08:00
putyy
567eb2903d perf: Batch export、operation item style optimization 2025-07-23 15:34:24 +08:00
putyy
31073eb57e perf: setting optimization 2025-07-23 10:42:12 +08:00
putyy
5613e21138 perf: index height calculation optimization 2025-07-23 09:41:20 +08:00
qiuzhiqian
0a516b2f3c 移除多余注释 2025-07-14 17:04:05 +08:00
xml
d3d8983307 fix: The certificate path in the ca-certificates.conf file on the deepin distribution is set incorrectly. 2025-07-14 17:04:05 +08:00
putyy
59e2b1b267 Merge branch 'wails' 2025-07-09 17:22:13 +08:00
putyy
4ebbc2347f perf: plugin optimize、add dartNode branding image 2025-07-09 17:16:56 +08:00
putyy
25ab8edd20 doc: delete DartNode 2025-07-09 17:16:56 +08:00
putyy
f4bc3c7b53 fix: Arch Linux certificate installation 2025-07-09 17:16:56 +08:00
putyy
ec89dc362f feat: Add version update detection 2025-07-09 17:16:56 +08:00
putyy
821d9949ab perf: Set up, download optimization, add description search... 2025-07-09 17:16:56 +08:00
putyy
e97120cf06 perf: Optimization of operation column 2025-07-09 17:16:56 +08:00
putyy
67f11d2b93 perf: intercept optimization 2025-07-09 17:16:56 +08:00
putyy
3be6b8cd91 doc: delete DartNode 2025-07-09 17:15:15 +08:00
putyy
db3ff8e0d2 fix: Arch Linux certificate installation 2025-07-07 15:22:06 +08:00
putyy
405d0bbdb2 feat: Add version update detection 2025-07-07 14:40:41 +08:00
putyy
6c21e37ce4 perf: Set up, download optimization, add description search... 2025-07-04 17:37:59 +08:00
putyy
b74e2a2bf6 perf: Optimization of operation column 2025-07-04 17:36:12 +08:00
putyy
deb3e83082 perf: intercept optimization 2025-06-08 11:20:56 +08:00
putyy
ff90c4ff03 perf: plugin optimize、add dartNode branding image 2025-06-08 11:07:53 +08:00
putyy
7f3d63532c perf: optimization 2025-06-08 11:07:53 +08:00
putyy
bd2fa75cde perf: Delete excess 2025-06-08 11:07:53 +08:00
putyy
27f9fb0def perf: install check 2025-06-08 11:07:53 +08:00
putyy
7a07456b2f Merge branch 'wails' of https://github.com/putyy/res-downloader into wails 2025-06-08 11:07:18 +08:00
putyy
3bce1f0332 perf: plugin optimize、add dartNode branding image 2025-06-08 11:07:11 +08:00
putyy
5a92d7beb7 perf: optimization 2025-05-27 17:20:01 +08:00
putyy
f28cb69826 perf: Delete excess 2025-05-21 17:59:35 +08:00
putyy
14d18ad310 perf: install check 2025-05-21 11:45:59 +08:00
putyy
ee6698a8e8 fix: upstream proxy settings 2025-05-20 17:17:08 +08:00
putyy
7f2b99b51f fix: upstream proxy settings 2025-05-20 17:16:05 +08:00
52 changed files with 2103 additions and 738 deletions

View File

@@ -32,7 +32,7 @@ Clean UI, easy to use, and supports a wide range of resource sniffing and downlo
- 📘 [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”*
> *If full, you can add WeChat `AmorousWorld` with a note “github”*
## 🧩 Download Links

View File

@@ -31,7 +31,7 @@
- 📘 [在线文档](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`,请备注“来源”*
> *群满时可加微信 `AmorousWorld`,请备注“github”*
## 🧩 下载地址
@@ -43,7 +43,6 @@
## 🖼️ 预览
![预览](docs/images/show.webp)
---
## 🚀 使用方法

BIN
build/.DS_Store vendored

Binary file not shown.

View File

@@ -24,7 +24,7 @@ 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
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/amd64/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
@@ -64,7 +64,7 @@ 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
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/arm64/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

View File

@@ -2,7 +2,7 @@ Package: res-downloader
Version: {{Version}}
Section: utils
Priority: optional
Architecture: amd64
Architecture: {{Architecture}}
Depends: libwebkit2gtk-4.0-37
Maintainer: putyy@qq.com
Homepage: https://github.com/putyy/res-downloader

View File

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

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/vrischmann/userdir"
"os"
"os/exec"
"path/filepath"
"regexp"
"res-downloader/core/shared"
@@ -25,6 +26,7 @@ type App struct {
PublicCrt []byte `json:"-"`
PrivateKey []byte `json:"-"`
IsProxy bool `json:"IsProxy"`
IsReset bool `json:"-"`
}
var (
@@ -35,6 +37,7 @@ var (
systemOnce *SystemSetup
proxyOnce *Proxy
httpServerOnce *HttpServer
ruleOnce *RuleSet
)
func GetApp(assets embed.FS, wjs string) *App {
@@ -51,6 +54,7 @@ func GetApp(assets embed.FS, wjs string) *App {
Version: version,
Description: "res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
Copyright: "Copyright © 2023~" + strconv.Itoa(time.Now().Year()),
IsReset: false,
PublicCrt: []byte(`-----BEGIN CERTIFICATE-----
MIIDwzCCAqugAwIBAgIUFAnC6268dp/z1DR9E1UepiWgWzkwDQYJKoZIhvcNAQEL
BQAwcDELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUNob25ncWluZzESMBAGA1UEBwwJ
@@ -117,6 +121,7 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
initResource()
initHttpServer()
initSystem()
initRule()
}
return appOnce
}
@@ -129,6 +134,10 @@ func (a *App) Startup(ctx context.Context) {
func (a *App) OnExit() {
a.UnsetSystemProxy()
globalLogger.Close()
if appOnce.IsReset {
err := a.ResetApp()
fmt.Println("err:", err)
}
}
func (a *App) installCert() (string, error) {
@@ -179,3 +188,24 @@ func (a *App) lock() error {
}
return nil
}
func (a *App) ResetApp() error {
exePath, err := os.Executable()
if err != nil {
return err
}
exePath, err = filepath.Abs(exePath)
if err != nil {
return err
}
_ = os.Remove(filepath.Join(appOnce.UserDir, "install.lock"))
_ = os.Remove(filepath.Join(appOnce.UserDir, "pass.cache"))
_ = os.Remove(filepath.Join(appOnce.UserDir, "config.json"))
_ = os.Remove(filepath.Join(appOnce.UserDir, "cert.crt"))
cmd := exec.Command(exePath)
cmd.Start()
return nil
}

View File

@@ -1,5 +1,9 @@
package core
import (
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type Bind struct {
}
@@ -14,3 +18,8 @@ func (b *Bind) Config() *ResponseData {
func (b *Bind) AppInfo() *ResponseData {
return httpServerOnce.buildResp(1, "ok", appOnce)
}
func (b *Bind) ResetApp() {
appOnce.IsReset = true
runtime.Quit(appOnce.ctx)
}

View File

@@ -2,8 +2,10 @@ package core
import (
"encoding/json"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
)
@@ -30,9 +32,12 @@ type Config struct {
AutoProxy bool `json:"AutoProxy"`
WxAction bool `json:"WxAction"`
TaskNumber int `json:"TaskNumber"`
DownNumber int `json:"DownNumber"`
UserAgent string `json:"UserAgent"`
UseHeaders string `json:"UseHeaders"`
InsertTail bool `json:"InsertTail"`
MimeMap map[string]MimeInfo `json:"MimeMap"`
Rule string `json:"Rule"`
}
var (
@@ -40,117 +45,171 @@ var (
)
func initConfig() *Config {
if globalConfig == nil {
def := `
{
"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": 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",
"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" }
if globalConfig != nil {
return globalConfig
}
defaultConfig := &Config{
Theme: "lightTheme",
Locale: "zh",
Host: "127.0.0.1",
Port: "8899",
Quality: 0,
SaveDirectory: getDefaultDownloadDir(),
FilenameLen: 0,
FilenameTime: true,
UpstreamProxy: "",
OpenProxy: false,
DownloadProxy: false,
AutoProxy: false,
WxAction: true,
TaskNumber: runtime.NumCPU() * 2,
DownNumber: 3,
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: "default",
InsertTail: true,
MimeMap: getDefaultMimeMap(),
Rule: "*",
}
rawDefaults, err := json.Marshal(defaultConfig)
if err != nil {
return globalConfig
}
storage := NewStorage("config.json", rawDefaults)
defaultConfig.storage = storage
globalConfig = defaultConfig
data, err := storage.Load()
if err != nil {
globalLogger.Esg(err, "load config failed, using defaults")
return globalConfig
}
var cacheMap map[string]interface{}
if err := json.Unmarshal(data, &cacheMap); err != nil {
globalLogger.Esg(err, "parse cached config failed, using defaults")
return globalConfig
}
var defaultMap map[string]interface{}
defaultBytes, _ := json.Marshal(defaultConfig)
_ = json.Unmarshal(defaultBytes, &defaultMap)
for k, v := range cacheMap {
if _, ok := defaultMap[k]; ok {
defaultMap[k] = v
}
}
finalBytes, err := json.Marshal(defaultMap)
if err != nil {
globalLogger.Esg(err, "marshal merged config failed")
return globalConfig
}
if err := json.Unmarshal(finalBytes, globalConfig); err != nil {
globalLogger.Esg(err, "unmarshal merged config to struct failed")
}
return globalConfig
}
func getDefaultMimeMap() map[string]MimeInfo {
return map[string]MimeInfo{
"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"},
"audio/x-mpegurl": {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"},
"application/octet-stream": {Type: "stream", Suffix: "default"},
}
}
`
def = strings.ReplaceAll(def, "__TaskNumber__", strconv.Itoa(runtime.NumCPU()*2))
globalConfig = &Config{
storage: NewStorage("config.json", []byte(def)),
}
defaultMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(def), &defaultMap)
func getDefaultDownloadDir() string {
usr, err := user.Current()
if err != nil {
return ""
}
data, err := globalConfig.storage.Load()
if err == nil {
var loadedMap map[string]interface{}
_ = json.Unmarshal(data, &loadedMap)
homeDir := usr.HomeDir
var downloadDir string
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")
switch runtime.GOOS {
case "windows", "darwin":
downloadDir = filepath.Join(homeDir, "Downloads")
case "linux":
downloadDir = filepath.Join(homeDir, "Downloads")
if xdgDir := os.Getenv("XDG_DOWNLOAD_DIR"); xdgDir != "" {
downloadDir = xdgDir
}
}
return globalConfig
if stat, err := os.Stat(downloadDir); err == nil && stat.IsDir() {
return downloadDir
}
return ""
}
func (c *Config) setConfig(config Config) {
oldProxy := c.UpstreamProxy
openProxy := c.OpenProxy
oldRule := c.Rule
c.Host = config.Host
c.Port = config.Port
c.Theme = config.Theme
@@ -165,12 +224,22 @@ func (c *Config) setConfig(config Config) {
c.DownloadProxy = config.DownloadProxy
c.AutoProxy = config.AutoProxy
c.TaskNumber = config.TaskNumber
c.DownNumber = config.DownNumber
c.WxAction = config.WxAction
c.UseHeaders = config.UseHeaders
if oldProxy != c.UpstreamProxy {
c.InsertTail = config.InsertTail
c.Rule = config.Rule
if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy {
proxyOnce.setTransport()
}
if oldRule != c.Rule {
err := ruleOnce.Load(c.Rule)
if err != nil {
globalLogger.Esg(err, "set rule failed")
}
}
mimeMux.Lock()
c.MimeMap = config.MimeMap
mimeMux.Unlock()
@@ -211,14 +280,20 @@ func (c *Config) getConfig(key string) interface{} {
return c.AutoProxy
case "TaskNumber":
return c.TaskNumber
case "DownNumber":
return c.DownNumber
case "WxAction":
return c.WxAction
case "UseHeaders":
return c.UseHeaders
case "InsertTail":
return c.InsertTail
case "MimeMap":
mimeMux.RLock()
defer mimeMux.RUnlock()
return c.MimeMap
case "Rule":
return c.Rule
default:
return nil
}

View File

@@ -1,13 +1,14 @@
package core
import (
"errors"
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"res-downloader/core/shared"
"strings"
"sync"
"time"
@@ -44,20 +45,27 @@ type FileDownloader struct {
totalTasks int
TotalSize int64
IsMultiPart bool
RetryOnError bool
Headers map[string]string
DownloadTaskList []*DownloadTask
progressCallback ProgressCallback
ctx context.Context
cancelFunc context.CancelFunc
}
func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader {
ctx, cancelFunc := context.WithCancel(context.Background())
return &FileDownloader{
Url: url,
FileName: filename,
totalTasks: totalTasks,
IsMultiPart: false,
RetryOnError: false,
TotalSize: 0,
Headers: headers,
DownloadTaskList: make([]*DownloadTask, 0),
ctx: ctx,
cancelFunc: cancelFunc,
}
}
@@ -71,12 +79,44 @@ func (fd *FileDownloader) buildClient() *http.Client {
}
return &http.Client{
Transport: transport,
Timeout: 60 * time.Second,
}
}
var forbiddenDownloadHeaders = map[string]struct{}{
"accept-encoding": {},
"content-length": {},
"host": {},
"connection": {},
"keep-alive": {},
"proxy-connection": {},
"transfer-encoding": {},
"sec-fetch-site": {},
"sec-fetch-mode": {},
"sec-fetch-dest": {},
"sec-fetch-user": {},
"sec-ch-ua": {},
"sec-ch-ua-mobile": {},
"sec-ch-ua-platform": {},
"if-none-match": {},
"if-modified-since": {},
"x-forwarded-for": {},
"x-real-ip": {},
}
func (fd *FileDownloader) setHeaders(request *http.Request) {
for key, value := range fd.Headers {
if globalConfig.UseHeaders == "default" {
lk := strings.ToLower(key)
if _, forbidden := forbiddenDownloadHeaders[lk]; forbidden {
continue
}
request.Header.Set(key, value)
continue
}
if strings.Contains(globalConfig.UseHeaders, key) {
request.Header.Set(key, value)
}
@@ -132,10 +172,9 @@ func (fd *FileDownloader) init() error {
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 = false
fd.TotalSize = -1
} else if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
fd.IsMultiPart = true
}
@@ -143,13 +182,18 @@ func (fd *FileDownloader) init() error {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("create directory failed: %w", err)
}
fd.FileName = shared.GetUniqueFileName(fd.FileName)
fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
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)
if fd.TotalSize > 0 {
if err := fd.File.Truncate(fd.TotalSize); err != nil {
fd.File.Close()
return fmt.Errorf("file truncate failed: %w", err)
}
}
return nil
}
@@ -182,10 +226,14 @@ func (fd *FileDownloader) createDownloadTasks() {
}
} else {
fd.totalTasks = 1
rangeEnd := int64(-1)
if fd.TotalSize > 0 {
rangeEnd = fd.TotalSize - 1
}
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
taskID: 0,
rangeStart: 0,
rangeEnd: fd.TotalSize - 1,
rangeEnd: rangeEnd,
})
}
}
@@ -233,6 +281,15 @@ func (fd *FileDownloader) startDownload() error {
}
if len(errArr) > 0 {
if !fd.RetryOnError && fd.IsMultiPart {
// 降级
fd.RetryOnError = true
fd.DownloadTaskList = []*DownloadTask{}
fd.totalTasks = 1
fd.IsMultiPart = false
fd.createDownloadTasks()
return fd.startDownload()
}
return fmt.Errorf("download failed with %d errors: %v", len(errArr), errArr[0])
}
@@ -253,11 +310,21 @@ func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan cha
return
}
if strings.Contains(err.Error(), "cancelled") {
errorChan <- err
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)
select {
case <-fd.ctx.Done():
errorChan <- fmt.Errorf("task %d cancelled during retry", task.taskID)
return
case <-time.After(RetryDelay):
}
}
}
@@ -265,7 +332,13 @@ func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan cha
}
func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error {
request, err := http.NewRequest("GET", fd.Url, nil)
select {
case <-fd.ctx.Done():
return fmt.Errorf("download cancelled")
default:
}
request, err := http.NewRequestWithContext(fd.ctx, "GET", fd.Url, nil)
if err != nil {
return fmt.Errorf("create request failed: %w", err)
}
@@ -292,33 +365,31 @@ func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *D
buf := make([]byte, 32*1024)
for {
select {
case <-fd.ctx.Done():
return fmt.Errorf("download cancelled")
default:
}
n, err := resp.Body.Read(buf)
if n > 0 {
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)
offset := task.rangeStart + task.downloadedSize
_, writeErr := fd.File.WriteAt(buf[:writeSize], offset)
if writeErr != nil {
return fmt.Errorf("write file failed at offset %d: %w", task.rangeStart+task.downloadedSize, writeErr)
return fmt.Errorf("write file failed at offset %d: %w", offset, writeErr)
}
task.downloadedSize += writeSize
progressChan <- ProgressChan{taskID: task.taskID, bytes: writeSize}
if task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
if fd.TotalSize > 0 && task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
return nil
}
}
if err != nil {
if err == io.EOF {
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
}
return fmt.Errorf("read response failed: %w", err)
@@ -331,22 +402,15 @@ func (fd *FileDownloader) verifyDownload() error {
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)
if fd.TotalSize > 0 {
_, err := fd.File.Stat()
if err != nil {
return fmt.Errorf("get file info failed: %w", err)
}
}
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
}
@@ -364,3 +428,17 @@ func (fd *FileDownloader) Start() error {
return err
}
func (fd *FileDownloader) Cancel() {
if fd.cancelFunc != nil {
fd.cancelFunc()
}
if fd.File != nil {
fd.File.Close()
}
if fd.FileName != "" {
_ = os.Remove(fd.FileName)
}
}

View File

@@ -4,18 +4,17 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"res-downloader/core/shared"
sysRuntime "runtime"
"strings"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type respData map[string]interface{}
@@ -85,8 +84,6 @@ func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
request.Header.Set("Range", rangeHeader)
}
//request.Header.Set("User-Agent", "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")
//request.Header.Set("Referer", parsedURL.Scheme+"://"+parsedURL.Host+"/")
resp, err := http.DefaultClient.Do(request)
if err != nil {
http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
@@ -94,12 +91,15 @@ func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
}
defer resp.Body.Close()
w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
w.WriteHeader(resp.StatusCode)
if contentRange := resp.Header.Get("Content-Range"); contentRange != "" {
w.Header().Set("Content-Range", contentRange)
for k, v := range resp.Header {
if strings.ToLower(k) == "access-control-allow-origin" {
continue
}
for _, vv := range v {
w.Header().Add(k, vv)
}
}
w.WriteHeader(resp.StatusCode)
_, err = io.Copy(w, resp.Body)
if err != nil {
@@ -206,42 +206,14 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
return
}
filePath := data.FilePath
var cmd *exec.Cmd
switch sysRuntime.GOOS {
case "darwin":
cmd = exec.Command("open", "-R", filePath)
case "windows":
cmd = exec.Command("explorer", "/select,", filePath)
case "linux":
cmd = exec.Command("nautilus", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("thunar", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("dolphin", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("pcmanfm", filePath)
if err := cmd.Start(); err != nil {
globalLogger.Err(err)
h.error(w, err.Error())
return
}
}
}
}
default:
h.error(w, "unsupported platform")
return
}
err = cmd.Start()
err = shared.OpenFolder(data.FilePath)
if err != nil {
globalLogger.Err(err)
h.error(w, err.Error())
return
}
h.success(w)
return
}
func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
@@ -352,18 +324,20 @@ func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
var data struct {
Sign string `json:"sign"`
Sign []string `json:"sign"`
}
err := json.NewDecoder(r.Body).Decode(&data)
if err == nil && data.Sign != "" {
resourceOnce.delete(data.Sign)
if err == nil && len(data.Sign) > 0 {
for _, v := range data.Sign {
resourceOnce.delete(v)
}
}
h.success(w)
}
func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
var data struct {
MediaInfo
shared.MediaInfo
DecodeStr string `json:"decodeStr"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
@@ -374,9 +348,27 @@ func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
h.success(w)
}
func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
var data struct {
shared.MediaInfo
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.error(w, err.Error())
return
}
err := resourceOnce.cancel(data.Id)
if err != nil {
h.error(w, err.Error())
return
}
h.success(w)
}
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
var data struct {
MediaInfo
shared.MediaInfo
Filename string `json:"filename"`
DecodeStr string `json:"decodeStr"`
}
@@ -394,7 +386,7 @@ func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
})
}
func (h *HttpServer) batchImport(w http.ResponseWriter, r *http.Request) {
func (h *HttpServer) batchExport(w http.ResponseWriter, r *http.Request) {
var data struct {
Content string `json:"content"`
}
@@ -408,6 +400,8 @@ func (h *HttpServer) batchImport(w http.ResponseWriter, r *http.Request) {
h.error(w, err.Error())
return
}
_ = shared.OpenFolder(fileName)
h.success(w, respData{
"file_name": fileName,
})

View File

@@ -17,8 +17,10 @@ func Middleware(next http.Handler) http.Handler {
func HandleApi(w http.ResponseWriter, r *http.Request) bool {
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.URL.Path != "/api/preview" {
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
@@ -56,10 +58,12 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
httpServerOnce.delete(w, r)
case "/api/download":
httpServerOnce.download(w, r)
case "/api/cancel":
httpServerOnce.cancel(w, r)
case "/api/wx-file-decode":
httpServerOnce.wxFileDecode(w, r)
case "/api/batch-import":
httpServerOnce.batchImport(w, r)
case "/api/batch-export":
httpServerOnce.batchExport(w, r)
case "/api/cert":
httpServerOnce.downCert(w, r)
}

View File

@@ -5,8 +5,10 @@ import (
"github.com/elazarl/goproxy"
gonanoid "github.com/matoous/go-nanoid/v2"
"net/http"
"path/filepath"
"res-downloader/core/shared"
"strconv"
"strings"
)
type DefaultPlugin struct {
@@ -26,7 +28,7 @@ func (p *DefaultPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http
}
func (p *DefaultPlugin) OnResponse(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 || (resp.StatusCode != 200 && resp.StatusCode != 206 && resp.StatusCode != 304) {
return resp
}
@@ -39,6 +41,13 @@ func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *
isAll, _ := p.bridge.GetResType("all")
isClassify, _ := p.bridge.GetResType(classify)
if suffix == "default" {
ext := filepath.Ext(filepath.Base(strings.Split(strings.Split(rawUrl, "?")[0], "#")[0]))
if ext != "" {
suffix = ext
}
}
urlSign := shared.Md5(rawUrl)
if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
@@ -51,7 +60,7 @@ func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *
Url: rawUrl,
UrlSign: urlSign,
CoverUrl: "",
Size: shared.FormatSize(value),
Size: value,
Domain: shared.GetTopLevelDomain(rawUrl),
Classify: classify,
Suffix: suffix,

View File

@@ -14,6 +14,9 @@ import (
"strings"
)
var qqMediaRegex = regexp.MustCompile(`get\s*media\(\)\{`)
var qqCommentRegex = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`)
type QqPlugin struct {
bridge *shared.Bridge
}
@@ -50,6 +53,9 @@ func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.
classify, _ := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
if strings.Contains(resp.Request.Header.Get("Origin"), "mp.weixin.qq.com") {
return nil
}
return resp
}
@@ -72,7 +78,7 @@ func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.
return respTemp
}
bodyStr := string(body)
newBody := regexp.MustCompile(`get\s*media\(\)\{`).
newBody := qqMediaRegex.
ReplaceAllString(bodyStr, `
get media(){
if(this.objectDesc){
@@ -85,7 +91,7 @@ func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.
`)
newBody = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`).
newBody = qqCommentRegex.
ReplaceAllString(newBody, `
async finderGetCommentDetail($1) {
var res = await$2;
@@ -119,13 +125,6 @@ func (p *QqPlugin) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (
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)
@@ -167,7 +166,7 @@ func (p *QqPlugin) handleMedia(body []byte) {
Url: rawUrl,
UrlSign: urlSign,
CoverUrl: "",
Size: "0",
Size: 0,
Domain: shared.GetTopLevelDomain(rawUrl),
Classify: "video",
Suffix: ".mp4",
@@ -185,16 +184,27 @@ func (p *QqPlugin) handleMedia(body []byte) {
res.ContentType = "image/png"
}
isAll, _ := p.bridge.GetResType("all")
isImage, _ := p.bridge.GetResType("image")
if res.Classify == "image" && !isImage && !isAll {
return
}
isVideo, _ := p.bridge.GetResType("video")
if res.Classify == "video" && !isVideo && !isAll {
return
}
if urlToken, ok := firstMedia["urlToken"].(string); ok {
res.Url += urlToken
}
switch size := firstMedia["fileSize"].(type) {
case float64:
res.Size = shared.FormatSize(size)
res.Size = size
case string:
if value, err := strconv.ParseFloat(size, 64); err == nil {
res.Size = shared.FormatSize(value)
res.Size = value
}
}

View File

@@ -21,23 +21,6 @@ type Proxy struct {
Is bool
}
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
}
var pluginRegistry = make(map[string]shared.Plugin)
func init() {
@@ -97,7 +80,14 @@ func (p *Proxy) Startup() {
//p.Proxy.KeepDestinationHeaders = true
//p.Proxy.Verbose = false
p.setTransport()
p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
//p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
p.Proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
if ruleOnce.shouldMitm(host) {
return goproxy.MitmConnect, host
}
return goproxy.OkConnect, host
})
p.Proxy.OnRequest().DoFunc(p.httpRequestEvent)
p.Proxy.OnResponse().DoFunc(p.httpResponseEvent)
}
@@ -105,7 +95,6 @@ func (p *Proxy) Startup() {
func (p *Proxy) setCa() error {
ca, err := tls.X509KeyPair(appOnce.PublicCrt, appOnce.PrivateKey)
if err != nil {
DialogErr("Failed to start proxy service 1")
return err
}
if ca.Leaf, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
@@ -131,10 +120,14 @@ func (p *Proxy) setTransport() {
IdleConnTimeout: 30 * time.Second,
}
p.Proxy.ConnectDial = nil
p.Proxy.ConnectDialWithReq = nil
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)
p.Proxy.ConnectDial = p.Proxy.NewConnectDialToProxy(globalConfig.UpstreamProxy)
}
}
p.Proxy.Tr = transport

View File

@@ -3,6 +3,7 @@ package core
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
@@ -22,29 +23,33 @@ type WxFileDecodeResult struct {
type Resource struct {
mediaMark sync.Map
tasks sync.Map
resType map[string]bool
resTypeMux sync.RWMutex
}
func initResource() *Resource {
if resourceOnce == nil {
resourceOnce = &Resource{
resType: map[string]bool{
"all": true,
"image": true,
"audio": true,
"video": true,
"m3u8": true,
"live": true,
"xls": true,
"doc": true,
"pdf": true,
},
}
resourceOnce = &Resource{}
resourceOnce.resType = resourceOnce.buildResType(globalConfig.MimeMap)
}
return resourceOnce
}
func (r *Resource) buildResType(mime map[string]MimeInfo) map[string]bool {
t := map[string]bool{
"all": true,
}
for _, item := range mime {
if _, ok := t[item.Type]; !ok {
t[item.Type] = true
}
}
return t
}
func (r *Resource) mediaIsMarked(key string) bool {
_, loaded := r.mediaMark.Load(key)
return loaded
@@ -56,29 +61,23 @@ func (r *Resource) markMedia(key string) {
func (r *Resource) getResType(key string) (bool, bool) {
r.resTypeMux.RLock()
defer r.resTypeMux.RUnlock()
value, ok := r.resType[key]
r.resTypeMux.RUnlock()
return value, ok
}
func (r *Resource) setResType(n []string) {
r.resTypeMux.Lock()
defer r.resTypeMux.Unlock()
r.resType = map[string]bool{
"all": false,
"image": false,
"audio": false,
"video": false,
"m3u8": false,
"live": false,
"xls": false,
"doc": false,
"pdf": false,
for key := range r.resType {
r.resType[key] = false
}
for _, value := range n {
r.resType[value] = true
if _, ok := r.resType[value]; ok {
r.resType[value] = true
}
}
r.resTypeMux.Unlock()
}
func (r *Resource) clear() {
@@ -89,13 +88,27 @@ func (r *Resource) delete(sign string) {
r.mediaMark.Delete(sign)
}
func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
func (r *Resource) cancel(id string) error {
if d, ok := r.tasks.Load(id); ok {
d.(*FileDownloader).Cancel()
r.tasks.Delete(id) // 可选:取消后清理
return nil
}
return errors.New("task not found")
}
func (r *Resource) download(mediaInfo shared.MediaInfo, decodeStr string) {
if globalConfig.SaveDirectory == "" {
return
}
go func(mediaInfo MediaInfo) {
go func(mediaInfo shared.MediaInfo) {
rawUrl := mediaInfo.Url
fileName := shared.Md5(rawUrl)
if v := shared.GetFileNameFromURL(rawUrl); v != "" {
fileName = v
}
if mediaInfo.Description != "" {
fileName = regexp.MustCompile(`[^\w\p{Han}]`).ReplaceAllString(mediaInfo.Description, "")
fileLen := globalConfig.FilenameLen
@@ -110,9 +123,13 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
}
if globalConfig.FilenameTime {
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted()+mediaInfo.Suffix)
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted())
} else {
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+mediaInfo.Suffix)
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName)
}
if !strings.HasSuffix(mediaInfo.SavePath, mediaInfo.Suffix) {
mediaInfo.SavePath = mediaInfo.SavePath + mediaInfo.Suffix
}
if strings.Contains(rawUrl, "qq.com") {
@@ -143,9 +160,13 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) {
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
}
r.tasks.Store(mediaInfo.Id, downloader)
err := downloader.Start()
mediaInfo.SavePath = downloader.FileName
if err != nil {
r.progressEventsEmit(mediaInfo, err.Error())
if !strings.Contains(err.Error(), "cancelled") {
r.progressEventsEmit(mediaInfo, err.Error())
}
return
}
if decodeStr != "" {
@@ -159,7 +180,7 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
}(mediaInfo)
}
func (r *Resource) parseHeaders(mediaInfo MediaInfo) (map[string]string, error) {
func (r *Resource) parseHeaders(mediaInfo shared.MediaInfo) (map[string]string, error) {
headers := make(map[string]string)
if hh, ok := mediaInfo.OtherData["headers"]; ok {
@@ -178,7 +199,7 @@ func (r *Resource) parseHeaders(mediaInfo MediaInfo) (map[string]string, error)
return headers, nil
}
func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string) (string, error) {
func (r *Resource) wxFileDecode(mediaInfo shared.MediaInfo, fileName, decodeStr string) (string, error) {
sourceFile, err := os.Open(fileName)
if err != nil {
return "", err
@@ -203,7 +224,7 @@ func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string)
return mediaInfo.SavePath, nil
}
func (r *Resource) progressEventsEmit(mediaInfo MediaInfo, args ...string) {
func (r *Resource) progressEventsEmit(mediaInfo shared.MediaInfo, args ...string) {
Status := shared.DownloadStatusError
Message := "ok"
@@ -236,10 +257,15 @@ func (r *Resource) decodeWxFile(fileName, decodeStr string) error {
byteCount := len(decodedBytes)
fileBytes := make([]byte, byteCount)
_, err = file.Read(fileBytes)
n, err := file.Read(fileBytes)
if err != nil && err != io.EOF {
return err
}
if n < byteCount {
byteCount = n
}
xorResult := make([]byte, byteCount)
for i := 0; i < byteCount; i++ {
xorResult[i] = decodedBytes[i] ^ fileBytes[i]

126
core/rule.go Normal file
View File

@@ -0,0 +1,126 @@
package core
import (
"bufio"
"net"
"strings"
"sync"
)
type Rule struct {
raw string
isNeg bool // 是否否定规则(以 ! 开头)
isWildcard bool // 是否为 *.domain 形式
isAll bool
domain string // 域名部分,不含 "*."
}
type RuleSet struct {
mu sync.RWMutex
rules []Rule
}
func initRule() *RuleSet {
if ruleOnce == nil {
ruleOnce = &RuleSet{}
err := ruleOnce.Load(globalConfig.Rule)
if err != nil {
globalLogger.Esg(err, "init rule failed")
return nil
}
}
return ruleOnce
}
func (r *RuleSet) Load(rs string) error {
reader := strings.NewReader(rs)
scanner := bufio.NewScanner(reader)
var rules []Rule
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
isNeg := false
if strings.HasPrefix(line, "!") {
isNeg = true
line = strings.TrimSpace(line[1:])
if line == "" {
continue
}
}
if line == "*" {
rules = append(rules, Rule{
raw: "*",
isAll: true,
isNeg: isNeg,
})
continue
}
isWildcard := false
domain := line
if strings.HasPrefix(line, "*.") {
isWildcard = true
domain = line[2:]
}
rules = append(rules, Rule{
raw: line,
isNeg: isNeg,
isWildcard: isWildcard,
domain: strings.ToLower(domain),
})
}
if err := scanner.Err(); err != nil {
return err
}
r.mu.Lock()
r.rules = rules
r.mu.Unlock()
return nil
}
// shouldMitm: 根据当前规则集判断是否对 host 做 MITM
// host 可能带端口example.com:443函数会只匹配 hostname 部分
// 返回 true => MITM解密false => 透传
func (r *RuleSet) shouldMitm(host string) bool {
h := host
if strings.HasPrefix(h, "[") {
if hostSplitIdx := strings.LastIndex(h, "]"); hostSplitIdx != -1 {
h = h[:hostSplitIdx+1]
}
}
if hp, _, err := net.SplitHostPort(host); err == nil {
h = hp
}
h = strings.ToLower(strings.Trim(h, "[]"))
r.mu.RLock()
defer r.mu.RUnlock()
action := false
for _, rule := range r.rules {
if rule.isAll {
action = !rule.isNeg
continue
}
if rule.isWildcard {
if h == rule.domain || strings.HasSuffix(h, "."+rule.domain) {
action = !rule.isNeg
}
continue
}
if h == rule.domain {
action = !rule.isNeg
}
}
return action
}

View File

@@ -5,7 +5,7 @@ type MediaInfo struct {
Url string
UrlSign string
CoverUrl string
Size string
Size float64
Domain string
Classify string
Suffix string

View File

@@ -3,10 +3,17 @@ package shared
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"golang.org/x/net/publicsuffix"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
sysRuntime "runtime"
"strings"
"time"
)
@@ -58,6 +65,45 @@ func IsDevelopment() bool {
return os.Getenv("APP_ENV") == "development"
}
func GetFileNameFromURL(rawUrl string) string {
parsedURL, err := url.Parse(rawUrl)
if err != nil {
return ""
}
fileName := path.Base(parsedURL.Path)
if fileName == "" || fileName == "/" {
return ""
}
if decoded, err := url.QueryUnescape(fileName); err == nil {
fileName = decoded
}
re := regexp.MustCompile(`[<>:"/\\|?*]`)
fileName = re.ReplaceAllString(fileName, "_")
fileName = strings.TrimRightFunc(fileName, func(r rune) bool {
return r == '.' || r == ' '
})
const maxFileNameLen = 255
runes := []rune(fileName)
if len(runes) > maxFileNameLen {
ext := path.Ext(fileName)
name := strings.TrimSuffix(fileName, ext)
runes = []rune(name)
if len(runes) > maxFileNameLen-len(ext) {
runes = runes[:maxFileNameLen-len(ext)]
}
name = string(runes)
fileName = name + ext
}
return fileName
}
func GetCurrentDateTimeFormatted() string {
now := time.Now()
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
@@ -68,3 +114,50 @@ func GetCurrentDateTimeFormatted() string {
now.Minute(),
now.Second())
}
func GetUniqueFileName(filePath string) string {
if !FileExist(filePath) {
return filePath
}
ext := filepath.Ext(filePath)
baseName := strings.TrimSuffix(filePath, ext)
count := 1
for {
newFileName := fmt.Sprintf("%s(%d)%s", baseName, count, ext)
if !FileExist(newFileName) {
return newFileName
}
count++
}
}
func OpenFolder(filePath string) error {
var cmd *exec.Cmd
switch sysRuntime.GOOS {
case "darwin":
cmd = exec.Command("open", "-R", filePath)
case "windows":
cmd = exec.Command("explorer", "/select,", filePath)
case "linux":
cmd = exec.Command("nautilus", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("thunar", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("dolphin", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("pcmanfm", filePath)
if err := cmd.Start(); err != nil {
return err
}
}
}
}
default:
return errors.New("unsupported platform")
}
return cmd.Start()
}

View File

@@ -88,13 +88,18 @@ func (s *SystemSetup) installCert() (string, error) {
}
certName := appOnce.AppName + ".crt"
var certPath string
if distro == "deepin" {
var updateCmd = []string{"update-ca-certificates"}
switch distro {
case "deepin":
certDir := "/usr/share/ca-certificates/" + appOnce.AppName
certPath = certDir + "/" + certName
s.runCommand([]string{"mkdir", "-p", certDir}, true)
} else {
case "arch":
certPath = "/usr/share/ca-certificates/trust-source/" + certName
updateCmd = []string{"update-ca-trust"}
default:
certPath = "/usr/local/share/ca-certificates/" + certName
}
@@ -112,7 +117,7 @@ func (s *SystemSetup) installCert() (string, error) {
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)}
echoCmd := []string{"bash", "-c", fmt.Sprintf("echo '%s/%s' >> %s", appOnce.AppName, 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 {
@@ -122,7 +127,7 @@ func (s *SystemSetup) installCert() (string, error) {
}
}
if output, err := s.runCommand([]string{"update-ca-certificates"}, true); err != nil {
if output, err := s.runCommand(updateCmd, true); err != nil {
errs.WriteString(fmt.Sprintf("update failed: %s\n%s\n", err.Error(), output))
} else {
isSuccess = true

View File

@@ -1,5 +1,5 @@
## 开启代理
- 安装完成后开启代理,如图:
- 安装完成后开启代理 (最新版本为“开启抓取”),如图:
![](images/examples-1.png ':size=50%')
## 拦截资源

View File

@@ -11,4 +11,8 @@
- 打开要捕获的源, 如:视频号、网页、小程序等等
- 返回软件首页即可看到资源列
!> windows安装先关闭所有安全管家之类的软件安装完成后首次使用需右键管理员打开
!> Mac如果无法拦截 请关闭防火墙
![](images/show.webp ':size=50%')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,5 +1,5 @@
## 下载安装文件
- windows下载.exe结尾的根据自己的系统架构下载合适的安装文件通常下载带有“x64-installer.exe”结尾的文件
- windows下载.exe结尾的根据自己的系统架构下载合适的安装文件通常下载带有“win_amd64.exe”或“x64-installer.exe”结尾的文件
- Mac下载.dmg结尾即可
- Linux根据系统类型下载对应的执行文件或安装文件

View File

@@ -7,6 +7,9 @@
- 比如只需要视频时就选择视频类型,可以多选
![more-1.png](images/more-2.png ':size=30%')
## 批量导出、批量导入使用场景
- 导出resd格式数据将txt文件发送到另外的电脑打开文件复制内容使用批量导入导入到新电脑
## 复制链接、视频解密
- 复制链接可用于第三方软件进行下载,下载完成后对该视频解密,点击“视频解密”选择用其他软件下载完成后的视频文件进行解密
![more-3.png](images/more-3.png ':size=30%')

View File

@@ -2,7 +2,7 @@
> 设置里面关闭全量拦截,将视频转发好友后打开
## 某某网址拦截不了?
> 本软件实现原理 & 初衷如下,并非万能的,所以有一些应用拦截不了很正常
> 本软件并非万能的,所以有一些应用拦截不了很正常,实现原理 & 初衷如下,
```
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
```
@@ -10,7 +10,7 @@
## 软件打不开了?之前可以打开
> 删除对应目录, 然后重启
```
## Mac执行
## Mac终端执行
rm -rf /Users/$(whoami)/Library/Preferences/res-downloader
## Windows手动删除以下目录Administrator为用户名 通常如下:
@@ -51,7 +51,8 @@ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keyc
## 拦截不到小程序中的资源
清理微信缓存,删除小程序后,重新打开
> 设置->存储空间->缓存
> 1.设置->存储空间->缓存
> 2.删除小程序相关缓存目录(自行搜索)
## 只拦截打开的视频号视频
关闭全量拦截,打开视频号视频详情,通常分享好友后打开的页面属于详情页

View File

@@ -7,11 +7,15 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Action: typeof import('./src/components/Action.vue')['default']
ActionDesc: typeof import('./src/components/ActionDesc.vue')['default']
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']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
@@ -31,18 +35,21 @@ declare module 'vue' {
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
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']
RouterView: typeof import('vue-router')['RouterView']
Screen: typeof import('./src/components/Screen.vue')['default']
ShowLoading: typeof import('./src/components/ShowLoading.vue')['default']
ShowOrEdit: typeof import('./src/components/ShowOrEdit.vue')['default']
Sider: typeof import('./src/components/layout/Sider.vue')['default']
}
}

BIN
frontend/src/.DS_Store vendored

Binary file not shown.

View File

@@ -2,8 +2,6 @@
<NConfigProvider class="h-full" :theme="theme" :locale="uiLocale">
<NaiveProvider>
<RouterView/>
<ShowLoading :isLoading="loading"/>
<Password v-model:showModal="showPassword" @submit="handlePassword"/>
</NaiveProvider>
<NGlobalStyle/>
<NModalProvider/>
@@ -14,19 +12,14 @@
import NaiveProvider from '@/components/NaiveProvider.vue'
import {darkTheme, lightTheme, zhCN, enUS} from 'naive-ui'
import {useIndexStore} from "@/stores"
import {computed, onMounted, ref} from "vue"
import {computed, onMounted} 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 {locale} = useI18n()
const theme = computed(() => {
if (store.globalConfig.Theme === "darkTheme") {
@@ -47,11 +40,6 @@ const uiLocale = computed(() => {
onMounted(async () => {
await store.init()
loading.value = true
handleInstall().then((is: boolean)=>{
loading.value = false
})
eventStore.init()
eventStore.addHandle({
@@ -68,34 +56,4 @@ onMounted(async () => {
}
})
})
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>

View File

@@ -92,6 +92,13 @@ export default {
data: data
})
},
cancel(data: object) {
return request({
url: 'api/cancel',
method: 'post',
data: data
})
},
download(data: object) {
return request({
url: 'api/download',
@@ -106,9 +113,9 @@ export default {
data: data
})
},
batchImport(data: object) {
batchExport(data: object) {
return request({
url: 'api/batch-import',
url: 'api/batch-export',
method: 'post',
data: data
})

View File

@@ -1,7 +1,5 @@
import type {AxiosResponse, InternalAxiosRequestConfig} from 'axios'
import axios from 'axios'
import {useIndexStore} from "@/stores";
import {computed} from "vue";
interface RequestOptions {
url: string
@@ -12,6 +10,7 @@ interface RequestOptions {
const instance = axios.create({
baseURL: "/",
timeout: 180000
})
instance.interceptors.request.use(

View File

@@ -7,4 +7,12 @@
#app {
width: 100vw;
height: 100vh;
}
.ellipsis-2 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}

View File

@@ -0,0 +1,109 @@
<template>
<div style="--wails-draggable:no-drag" class="grid grid-cols-3 gap-1.5">
<n-icon
size="30"
class="text-emerald-600 dark:text-emerald-400 bg-emerald-500/20 dark:bg-emerald-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-emerald-500/40 transition-colors"
@click="action('down')"
>
<DownloadOutline/>
</n-icon>
<n-icon
size="28"
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
@click="action('delete')"
>
<TrashOutline/>
</n-icon>
<NPopover placement="bottom" trigger="hover">
<template #trigger>
<NIcon size="30" class="text-sky-500 dark:text-sky-300 bg-sky-500/20 dark:bg-sky-200/30 rounded-full flex items-center justify-center p-2 cursor-pointer hover:bg-sky-200/40 transition-colors">
<GridSharp/>
</NIcon>
</template>
<div class="flex flex-col">
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.Status === 'running' || row.Status === 'pending'" @click="action('cancel')">
<n-icon
size="28"
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
>
<CloseOutline/>
</n-icon>
<span class="ml-1">{{ t("index.cancel_down") }}</span>
</div>
<div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('copy')">
<n-icon
size="28"
class="text-blue-300 dark:text-blue-300 bg-blue-300/20 dark:bg-blue-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-300/40 transition-colors"
>
<LinkOutline/>
</n-icon>
<span class="ml-1">{{ t("index.copy_link") }}</span>
</div>
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.Classify !== 'live' && row.Classify !== 'm3u8'" @click="action('open')">
<n-icon
size="28"
class="text-blue-500 dark:text-blue-200 bg-blue-400/20 dark:bg-blue-400/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-400/40 transition-colors"
>
<GlobeOutline/>
</n-icon>
<span class="ml-1">{{ t("index.open_link") }}</span>
</div>
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.DecodeKey" @click="action('decode')">
<n-icon
size="28"
class="text-orange-400 dark:text-red-300 bg-orange-500/20 dark:bg-orange-200/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-orange-200/40 transition-colors"
>
<LockOpenSharp/>
</n-icon>
<span class="ml-1">{{ t("index.video_decode") }}</span>
</div>
<div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('json')">
<n-icon
size="28"
class="text-sky-400 dark:text-sky-200 bg-sky-500/20 dark:bg-sky-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-sky-500/40 transition-colors"
>
<CopyOutline/>
</n-icon>
<span class="ml-1">{{ t("index.copy_data") }}</span>
</div>
</div>
</NPopover>
</div>
</template>
<script setup lang="ts">
import {useI18n} from 'vue-i18n'
import {
DownloadOutline,
CopyOutline,
GlobeOutline,
LockOpenSharp,
LinkOutline,
GridSharp,
CloseOutline,
TrashOutline
} from "@vicons/ionicons5"
const {t} = useI18n()
const props = defineProps<{
row: any,
index: number,
}>()
const emits = defineEmits(["action"])
const action = (type: string) => {
if (type === 'down' && (props.row.Classify === 'live' || props.row.Classify === 'm3u8')) {
window?.$message?.error(t("index.download_no_tip"))
return
}
emits('action', props.row, props.index, type)
}
</script>

View File

@@ -0,0 +1,107 @@
<template>
<div class="flex items-center">
<span>
{{ t('index.operation') }}
</span>
<NPopover trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
<div class="flex flex-col">
<div class="flex items-center justify-start p-1.5">
<n-icon size="28"
class="text-emerald-600 dark:text-emerald-400 bg-emerald-500/20 dark:bg-emerald-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-emerald-500/40 transition-colors">
<DownloadOutline/>
</n-icon>
<span class="ml-1">{{ t("index.direct_download") }}</span>
</div>
<div class="flex items-center justify-start p-1.5">
<n-icon size="28"
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors">
<CloseOutline/>
</n-icon>
<span class="ml-1">{{ t("index.cancel_down") }}</span>
</div>
<div class="flex items-center justify-start p-1.5">
<n-icon
size="28"
class="text-blue-600 dark:text-blue-300 bg-blue-500/20 dark:bg-blue-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-500/40 transition-colors"
>
<LinkOutline/>
</n-icon>
<span class="ml-1">{{ t("index.copy_link") }}</span>
</div>
<div class="flex items-center justify-start p-1.5">
<n-icon
size="28"
class="text-blue-500 dark:text-blue-200 bg-blue-400/20 dark:bg-blue-400/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-blue-400/40 transition-colors"
>
<GlobeOutline/>
</n-icon>
<span class="ml-1">{{ t("index.open_link") }}</span>
</div>
<div class="flex items-center justify-start p-1.5">
<n-icon
size="28"
class="text-orange-400 dark:text-red-300 bg-orange-500/20 dark:bg-orange-200/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-orange-200/40 transition-colors"
>
<LockOpenSharp/>
</n-icon>
<span class="ml-1">{{ t("index.video_decode") }}</span>
</div>
<div class="flex items-center justify-start p-1.5">
<n-icon
size="28"
class="text-sky-400 dark:text-sky-200 bg-sky-500/20 dark:bg-sky-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-sky-500/40 transition-colors"
>
<CopyOutline/>
</n-icon>
<span class="ml-1">{{ t("index.copy_data") }}</span>
</div>
<div class="flex items-center justify-start p-1.5">
<n-icon
size="28"
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
>
<TrashOutline/>
</n-icon>
<span class="ml-1">{{ t("index.delete_row") }}</span>
</div>
<div class="flex items-center justify-start p-1.5">
<n-icon
size="28"
class="text-sky-500 dark:text-sky-300 bg-sky-500/20 dark:bg-sky-200/30 rounded-full flex items-center justify-center p-2 cursor-pointer hover:bg-sky-200/40 transition-colors"
>
<GridSharp/>
</n-icon>
<span class="ml-1">{{ t("index.more_operation") }}</span>
</div>
</div>
</NPopover>
</div>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n"
import {
CopyOutline,
DownloadOutline,
GlobeOutline,
HelpCircleOutline,
LinkOutline,
LockOpenSharp,
GridSharp,
CloseOutline,
TrashOutline
} from "@vicons/ionicons5"
const {t} = useI18n()
</script>

View File

@@ -38,7 +38,7 @@
<div>{{ store.appInfo.Copyright }}</div>
<div class="flex">
<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(certUrl)">{{ t('footer.cert_download') }}</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>

View File

@@ -84,7 +84,7 @@ const playFlvStream = () => {
try {
if (!flvjs.isSupported() || !videoPlayer.value) return
flvPlayer = flvjs.createPlayer({ type: "flv", url: props.previewRow.Url })
flvPlayer = flvjs.createPlayer({ type: "flv", url: window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(props.previewRow.Url) })
flvPlayer.attachMediaElement(videoPlayer.value)
flvPlayer.load()
flvPlayer.play()
@@ -105,7 +105,7 @@ const setupVideoJsPlayer = () => {
}
player.src({
src: props.previewRow.Url,
src: window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(props.previewRow.Url),
type: props.previewRow.ContentType,
withCredentials: true,
})
@@ -113,7 +113,7 @@ const setupVideoJsPlayer = () => {
}
const playVideoWithoutTotalLength = () => {
rowUrl = buildUrlWithParams(props.previewRow.Url)
rowUrl = window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(buildUrlWithParams(props.previewRow.Url))
mediaSource = new MediaSource()
videoPlayer.value.src = URL.createObjectURL(mediaSource)
videoPlayer.value.play()
@@ -141,7 +141,6 @@ const buildUrlWithParams = (url: string) => {
}
const handleSeeking = () => {
console.log('handleSeeking')
const currentTime = videoPlayer.value.currentTime
const bufferedEnd = videoPlayer.value.buffered.end(videoPlayer.value.buffered.length - 1)

View File

@@ -1,39 +0,0 @@
<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,
}>()
const emits = defineEmits(["action"])
const action = (type: string) => {
emits('action', props.row, props.index, type)
}
</script>

View File

@@ -0,0 +1,59 @@
<template>
<div
class="min-h-6"
@click="handleOnClick"
>
<n-input
v-if="isEdit"
ref="inputRef"
:value="inputValue"
@update:value="v => inputValue = v"
@change="handleChange"
@blur="handleChange"
/>
<n-tooltip
v-else
trigger="hover"
placement="top"
>
<template #trigger>
<div class="ellipsis-2">{{ inputValue }}</div>
</template>
<div class="ellipsis-2">{{ inputValue }}</div>
</n-tooltip>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue'
import type { InputInst } from 'naive-ui'
interface OnUpdateValue {
(value: string): void
}
const props = defineProps<{
value: string | number
onUpdateValue?: OnUpdateValue
}>()
const isEdit = ref(false)
const inputRef = ref<InputInst | null>(null)
const inputValue = ref(String(props.value))
watch(
() => props.value,
v => inputValue.value = String(v)
)
function handleOnClick() {
isEdit.value = true
nextTick(() => inputRef.value?.focus())
}
function handleChange() {
props.onUpdateValue?.(String(inputValue.value))
isEdit.value = false
}
</script>

View File

@@ -2,7 +2,12 @@
<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" :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 class="relative flex items-center justify-center cursor-pointer" @click="handleFooterUpdate('github')">
<img class="w-12 h-12 rounded-full transition-transform duration-300 hover:scale-105 dark" src="@/assets/image/logo.png" alt="res-downloader logo"/>
<span class="absolute right-[-25px] top-0 font-semibold rounded-full bg-red-500 text-white dark:bg-red-600 dark:text-gray-100 text-[10px] px-1.5 py-0.5 animate-pulse" v-if="showUpdate">
New
</span>
</div>
</div>
<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">
@@ -15,7 +20,7 @@
:on-after-leave="() => { showAppName = false }"
:collapsed-width="70"
:collapsed="collapsed"
:width="140"
:width="envInfo.platform==='linux' ? 160 : 140"
:native-scrollbar="false"
:inverted="inverted"
:on-update:collapsed="collapsedChange"
@@ -66,6 +71,8 @@ import Footer from "@/components/Footer.vue"
import Screen from "@/components/Screen.vue"
import {BrowserOpenURL} from "../../../wailsjs/runtime"
import {useI18n} from "vue-i18n"
import request from "@/api/request"
import {compareVersions} from "@/func"
const {t} = useI18n()
const route = useRoute()
@@ -77,6 +84,7 @@ const showAppInfo = ref(false)
const menuValue = ref(route.fullPath.substring(1))
const store = useIndexStore()
const is = ref(false)
const showUpdate = ref(false)
const envInfo = store.envInfo
@@ -98,6 +106,13 @@ onMounted(()=>{
collapsed.value = JSON.parse(collapsedCache).collapsed
}
is.value = true
request({
url: 'https://res.putyy.com/version.json?v=' + Date.now(),
method: 'get',
}).then((res)=>{
showUpdate.value = compareVersions(res.version, store.appInfo.Version) === 1
})
})
const renderIcon = (icon: any) => {
@@ -179,8 +194,24 @@ const handleFooterUpdate = (key: string, item?: MenuOption) => {
}
const collapsedChange = (value: boolean)=>{
console.log("collapsedChange",value)
collapsed.value = value
localStorage.setItem("collapsed", JSON.stringify({collapsed: value}))
}
</script>
</script>
<style scoped>
@keyframes pulse {
0% {
transform: scale(0.9);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0.9);
}
}
.animate-pulse {
animation: pulse 2s infinite;
}
</style>

View File

@@ -1,7 +0,0 @@
export const DwStatus = {
ready: "就绪",
running: "运行中",
error: "错误",
done: "完成",
handle: "已下载,后续处理",
}

40
frontend/src/func.ts Normal file
View File

@@ -0,0 +1,40 @@
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/
const domainRegex = /^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,11}?$/
const localhostRegex = /^localhost$/
export const compareVersions = (v1: string, v2: string) => {
const parts1 = v1.split('.').map(Number)
const parts2 = v2.split('.').map(Number)
const maxLength = Math.max(parts1.length, parts2.length)
for (let i = 0; i < maxLength; i++) {
const num1 = parts1[i] || 0
const num2 = parts2[i] || 0
if (num1 < num2) return -1
if (num1 > num2) return 1
}
return 0
}
export const isValidHost = (host: string) => {
return ipv4Regex.test(host) || domainRegex.test(host) || localhostRegex.test(host)
}
export const isValidPort = (port: number) => {
const portNumber = Number(port)
return Number.isInteger(portNumber) && portNumber > 1024 && portNumber < 65535
}
export const formatSize = (size: number | string) => {
if (typeof size === "string") return size
if (size > 1048576) {
return (size / 1048576).toFixed(2) + 'MB';
}
if (size > 1024) {
return (size / 1024).toFixed(2) + 'KB';
}
return Math.floor(size) + 'b';
}

View File

@@ -33,24 +33,30 @@
"close_grab": "Stop Grabbing",
"grab_type": "Grab Type",
"clear_list": "Clear List",
"clear_list_tip": "Clear all records?",
"remember_clear_choice": "Remember this selection and clear it next time",
"batch_download": "Batch Download",
"batch_export": "Batch Export",
"batch_import": "Batch Import",
"export_url": "Export Url",
"import_success": "Export Success",
"total_resources": "total of {count} resources",
"all": "All",
"image": "Image",
"audio": "Audio",
"video": "Video",
"m3u8": "M3U8",
"live": "Live Stream",
"stream": "Data Stream",
"xls": "Spreadsheet",
"doc": "Document",
"pdf": "PDF",
"font": "Font",
"domain": "Domain",
"choice": "choice",
"type": "Type",
"preview": "Preview",
"preview_tip": "Preview not supported",
"preview_tip": "Cannot preview",
"status": "Status",
"description": "Description",
"resource_size": "Resource Size",
@@ -58,23 +64,38 @@
"save_path_empty": "Please set save location",
"operation": "Operation",
"ready": "Ready",
"pending": "Pending",
"running": "Running",
"error": "Error",
"done": "Done",
"handle": "Post Processing",
"direct_download": "Download",
"download_success": "Download Success",
"download_no_tip": "This type of download is not supported yet. Please copy the link and use other tools to download.",
"copy_link": "Copy Link",
"copy_data": "Copy Data",
"open_link": "Open Link",
"open_file": "Open File",
"delete_row": "Delete Row",
"delete_tip": "Running tasks cannot be deleted",
"cancel_down": "Cancel Download",
"more_operation": "More Operations",
"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"
"import_empty": "Please enter data to import",
"win_install_tip": "For the first time using this software, please right-click and select 'Run as administrator'",
"download_queued": "has been added to the queue, current queue length{count}",
"search": "Search",
"search_description": "Keyword Search...",
"start_err_tip": "Error Message",
"start_err_content": "The current startup process has encountered an issue. Do you want to reset the application?",
"start_err_positiveText": "Clear cache and restart",
"start_err_negativeText": "Close the software",
"reset_app_tip": "This operation will delete intercepted data and data related to this application. Please proceed with caution!"
},
"setting": {
"restart_tip": "Keep default if unsure, please restart software after modification",
@@ -88,6 +109,8 @@
"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",
"insert_tail": "Insert tail",
"insert_tail_tip": "Intercept whether new data is added to the end of the list",
"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",
@@ -95,9 +118,17 @@
"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",
"down_number": "Download Number",
"down_number_tip": "Number of downloads executed simultaneously",
"use_headers_tip": "Default system filtering, Define headers for downloads, comma separated",
"mime_map": "Intercept Rules",
"mime_map_tip": "JSON format, keep default if unsure"
"mime_map_tip": "JSON format, keep default if unsure, please restart software after modification",
"domain_rule": "Domain Rule",
"domain_rule_tip": "Default * matches all domains, One line for each rulesupports the following: \n*.qq.com\nvideo.qq.com\nexample.com\n\n# Exclude\n!static.qq.com",
"port_format_error": "port format error",
"host_format_error": "host format error",
"basic_setting": "Basic Setting",
"advanced_setting": "Advanced Setting"
},
"footer": {
"title": "About Us",
@@ -105,7 +136,7 @@
"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",
"cert_download": "Certificate Download",
"source_code": "Source Code",
"help": "Issues",
"update_log": "Update Log"

View File

@@ -33,24 +33,30 @@
"close_grab": "关闭抓取",
"grab_type": "抓取类型",
"clear_list": "清空列表",
"clear_list_tip": "清空所有记录?",
"remember_clear_choice": "记住此选择,下次直接清除",
"batch_download": "批量下载",
"batch_export": "批量导出",
"batch_import": "批量导入",
"export_url": "导出链接",
"import_success": "导出成功",
"total_resources": "共{count}个资源",
"all": "全部",
"image": "图片",
"audio": "音频",
"video": "视频",
"m3u8": "m3u8",
"live": "直播流",
"stream": "流数据",
"xls": "表格",
"doc": "文档",
"pdf": "pdf",
"font": "字体",
"domain": "域",
"choice": "已选",
"type": "类型",
"preview": "预览",
"preview_tip": "暂不支持预览",
"preview_tip": "无法预览",
"status": "状态",
"description": "描述",
"resource_size": "资源大小",
@@ -58,23 +64,38 @@
"save_path_empty": "请设置保存位置",
"operation": "操作",
"ready": "就绪",
"pending": "待处理",
"running": "运行中",
"error": "错误",
"done": "完成",
"handle": "后续处理",
"direct_download": "直接下载",
"download_success": "下载成功",
"download_no_tip": "该类型暂不支持下载,请复制链接后使用其他工具下载",
"copy_link": "复制链接",
"copy_data": "复制数据",
"open_link": "打开链接",
"open_file": "打开文件",
"delete_row": "删除记录",
"delete_tip": "运行中任务无法删除",
"cancel_down": "取消下载",
"more_operation": "更多操作",
"video_decode": "视频解密",
"video_decode_loading": "解密中",
"video_decode_no": "无法解密",
"video_decode_success": "解密成功",
"use_data": "请选择需要的数据",
"import_placeholder": "添加多个时,请确保每行只有一个(每个链接回车换行)",
"import_empty": "请输入需要导入的数据"
"import_empty": "请输入需要导入的数据",
"win_install_tip": "首次启用本软件,请使用鼠标右键选择以管理员身份运行",
"download_queued": "已加入队列,当前队列长度:{count}",
"search": "搜索",
"search_description": "关键字搜索...",
"start_err_tip": "错误提示",
"start_err_content": "当前启动过程遇到了问题,是否重置应用?",
"start_err_positiveText": "清理缓存并重启",
"start_err_negativeText": "关闭软件",
"reset_app_tip": "此操作会删除已拦截数据以及本应用相关数据,请谨慎操作!"
},
"setting": {
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",
@@ -82,12 +103,14 @@
"filename_rules": "文件命名",
"filename_rules_tip": "输入框控制文件命名的长度(不含时间、0为无效此选项有描述信息时有效),开关控制文件末尾是否添加时间标识",
"auto_proxy": "自动拦截",
"auto_proxy_tip": "打开软件时动启用拦截",
"auto_proxy_tip": "打开软件时动启用拦截",
"quality": "清晰度",
"quality_value": "默认(推荐),超清,高画质,中画质,低画质",
"quality_tip": "视频号有效",
"full_intercept": "全量拦截",
"full_intercept_tip": "微信视频号是否全量拦截,否:只拦截视频详情",
"insert_tail": "添入尾部",
"insert_tail_tip": "拦截到新数据是否添加到列表尾部",
"upstream_proxy": "上游代理",
"upstream_proxy_tip": "用于结合其他代理工具,格式: http://username:password@your.proxy.server:port",
"download_proxy": "下载代理",
@@ -95,9 +118,17 @@
"user_agent_tip": "如不清楚请保持默认",
"connections": "连接数",
"connections_tip": "如不清楚请保持默认通常CPU核心数*2用于加速下载",
"use_headers_tip": "定义下载时可使用的header参数逗号分割",
"down_number": "下载数",
"down_number_tip": "同时进行的下载数量",
"use_headers_tip": "默认系统过滤定义下载时可使用的header参数逗号分割",
"mime_map": "拦截规则",
"mime_map_tip": "json格式如果不清楚保持默认就行"
"mime_map_tip": "json格式如果不清楚保持默认就行,修改后请重启软件",
"domain_rule": "域名规则",
"domain_rule_tip": "默认*匹配所有域,每个规则一行,支持如下: \n*.qq.com\nvideo.qq.com\nexample.com\n\n# 排除\n!static.qq.com",
"port_format_error": "port 格式错误",
"host_format_error": "host 格式错误",
"basic_setting": "基础设置",
"advanced_setting": "高级设置"
},
"footer": {
"title": "关于我们",
@@ -105,7 +136,7 @@
"support": "支持市面上几乎所有的网络应用",
"application": "抖音,快手,小红书,视频号,小程序,公众号,酷狗音乐,QQ音乐,QQ微视,......",
"forum": "论坛",
"cert": "证书",
"cert_download": "证书下载",
"source_code": "软件源码",
"help": "帮助支持",
"update_log": "更新日志"

View File

@@ -29,9 +29,12 @@ export const useIndexStore = defineStore("index-store", () => {
AutoProxy: false,
WxAction: false,
TaskNumber: 8,
DownNumber: 3,
UserAgent: "",
UseHeaders: "",
MimeMap: {}
InsertTail: true,
MimeMap: {},
Rule: "*"
})
const envInfo = ref({
@@ -40,8 +43,6 @@ export const useIndexStore = defineStore("index-store", () => {
arch: "",
});
const tableHeight = ref(800)
const isProxy = ref(false)
const baseUrl = ref("")
@@ -59,10 +60,8 @@ export const useIndexStore = defineStore("index-store", () => {
globalConfig.value = Object.assign({}, globalConfig.value, res.data)
})
baseUrl.value = "http://"+globalConfig.value.Host + ":" +globalConfig.value.Port
baseUrl.value = "http://127.0.0.1:" +globalConfig.value.Port
window.$baseUrl = baseUrl.value
window.addEventListener("resize", handleResize);
handleResize()
}
const setConfig = (formValue: Object) => {
@@ -70,10 +69,6 @@ export const useIndexStore = defineStore("index-store", () => {
appApi.setConfig(globalConfig.value)
}
const handleResize = () => {
tableHeight.value = document.documentElement.clientHeight || window.innerHeight
}
const openProxy = async () => {
return appApi.openSystemProxy().then(handleProxy)
}
@@ -93,7 +88,6 @@ export const useIndexStore = defineStore("index-store", () => {
return {
appInfo,
globalConfig,
tableHeight,
isProxy,
envInfo,
baseUrl,

View File

@@ -26,9 +26,12 @@ export namespace appType {
AutoProxy: boolean
WxAction: boolean
TaskNumber: number
DownNumber: number
UserAgent: string
UseHeaders: string
InsertTail: boolean
MimeMap: { [key: string]: MimeMap }
Rule: string
}
interface MediaInfo {
@@ -36,7 +39,7 @@ export namespace appType {
Url: string
UrlSign: string
CoverUrl: string
Size: string
Size: number
Domain: string
Classify: string
Suffix: string

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +1,237 @@
<template>
<div class="h-full relative p-5 overflow-y-auto [&::-webkit-scrollbar]:hidden" :key="renderKey">
<NForm
:model="formValue"
size="medium"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
style="--wails-draggable:no-drag"
class="w-[700px]"
>
<NFormItem label="Host" path="Host">
<NInput v-model:value="formValue.Host" placeholder="127.0.0.1"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.restart_tip") }}
</NTooltip>
</NFormItem>
<NTabs type="line" animated>
<NTabPane name="basic" :tab="t('setting.basic_setting')">
<NForm
:model="formValue"
size="medium"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
style="--wails-draggable:no-drag"
class="w-[700px]"
>
<NFormItem :label="t('setting.save_dir')" path="SaveDirectory">
<NInput :value="formValue.SaveDirectory" :placeholder="t('setting.save_dir')"/>
<NButton strong secondary type="primary" @click="selectDir" class="ml-1">{{ t('common.select') }}</NButton>
</NFormItem>
<NFormItem label="Port" path="Port">
<NInput v-model:value="formValue.Port" placeholder="8899"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.restart_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.filename_rules')" path="FilenameLen">
<NInputNumber v-model:value="formValue.FilenameLen" :min="0" :max="9999" placeholder="0"/>
<NSwitch v-model:value="formValue.FilenameTime" class="ml-1"></NSwitch>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.filename_rules_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.upstream_proxy')" path="UpstreamProxy">
<NInput v-model:value="formValue.UpstreamProxy" placeholder="http://127.0.0.1:7890"/>
<NSwitch v-model:value="formValue.OpenProxy" class="ml-1"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.upstream_proxy_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.quality')" path="Quality">
<NSelect v-model:value="formValue.Quality" :options="options"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.quality_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.save_dir')" path="SaveDirectory">
<NInput :value="formValue.SaveDirectory" :placeholder="t('setting.save_dir')"/>
<NButton strong secondary type="primary" @click="selectDir" class="ml-1">{{ t('common.select') }}</NButton>
</NFormItem>
<NFormItem :label="t('setting.auto_proxy')" path="AutoProxy">
<NSwitch v-model:value="formValue.AutoProxy"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.auto_proxy_tip") }}
</NTooltip>
</NFormItem>
<div class="grid grid-cols-2">
<NFormItem :label="t('setting.filename_rules')" path="FilenameLen">
<NInputNumber v-model:value="formValue.FilenameLen" :min="0" :max="9999" placeholder="0"/>
<NSwitch v-model:value="formValue.FilenameTime" class="ml-1"></NSwitch>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.filename_rules_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.full_intercept')" path="WxAction">
<NSwitch v-model:value="formValue.WxAction"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.full_intercept_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.quality')" path="Quality">
<NSelect v-model:value="formValue.Quality" :options="options"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.quality_tip") }}
</NTooltip>
</NFormItem>
</div>
<NFormItem :label="t('setting.insert_tail')" path="InsertTail">
<NSwitch v-model:value="formValue.InsertTail"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.insert_tail_tip") }}
</NTooltip>
</NFormItem>
<div class="grid grid-cols-2 gap-4">
<NFormItem :label="t('setting.auto_proxy')" path="AutoProxy">
<NSwitch v-model:value="formValue.AutoProxy"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.auto_proxy_tip") }}
</NTooltip>
</NFormItem>
<NFormItem >
<n-popconfirm @positive-click="resetHandle">
<template #trigger>
<NButton tertiary type="error" style="--wails-draggable:no-drag">
{{ t("index.start_err_positiveText") }}
</NButton>
</template>
{{t("index.reset_app_tip")}}
</n-popconfirm>
</NFormItem>
</NForm>
</NTabPane>
<NFormItem :label="t('setting.full_intercept')" path="WxAction">
<NSwitch v-model:value="formValue.WxAction"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.full_intercept_tip") }}
</NTooltip>
</NFormItem>
</div>
<NTabPane name="advanced" :tab="t('setting.advanced_setting')">
<NForm
:model="formValue"
size="medium"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
style="--wails-draggable:no-drag"
class="w-[700px]"
>
<NFormItem label="Host" path="Host" :validation-status="hostValidationFeedback==='' ? undefined : 'error'" :feedback="hostValidationFeedback">
<NInput v-model:value="formValue.Host" placeholder="127.0.0.1"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.restart_tip") }}
</NTooltip>
</NFormItem>
<div class="grid grid-cols-2 gap-4">
<NFormItem :label="t('setting.download_proxy')" path="DownloadProxy">
<NSwitch v-model:value="formValue.DownloadProxy"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.download_proxy_tip") }}
</NTooltip>
</NFormItem>
<NFormItem label="Port" path="Port" :validation-status="portValidationFeedback==='' ? undefined : 'error'" :feedback="portValidationFeedback">
<NInput v-model:value="formValue.Port" placeholder="8899"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.restart_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.connections')" path="TaskNumber">
<NInputNumber v-model:value="formValue.TaskNumber" :min="2" :max="64"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.connections_tip") }}
</NTooltip>
</NFormItem>
</div>
<NFormItem :label="t('setting.upstream_proxy')" path="UpstreamProxy">
<NInput v-model:value="formValue.UpstreamProxy" placeholder="http://127.0.0.1:7890"/>
<NSwitch v-model:value="formValue.OpenProxy" class="ml-1"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.upstream_proxy_tip") }}
</NTooltip>
</NFormItem>
<NFormItem label="UserAgent" path="UserAgent">
<NInput v-model:value="formValue.UserAgent" placeholder="默认UserAgent"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.user_agent_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.download_proxy')" path="DownloadProxy">
<NSwitch v-model:value="formValue.DownloadProxy"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.download_proxy_tip") }}
</NTooltip>
</NFormItem>
<NFormItem label="Headers" path="Headers">
<NInput v-model:value="formValue.UseHeaders" placeholder="User-Agent,Referer,Authorization,Cookie"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.use_headers_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.connections')" path="TaskNumber">
<NInputNumber v-model:value="formValue.TaskNumber" :min="2" :max="64"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.connections_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
<NInput
v-model:value="MimeMap"
type="textarea"
rows="11"
placeholder='{"video/mp4": { "Type": "video","Suffix": ".mp4"}}'
/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.mime_map_tip") }}
</NTooltip>
</NFormItem>
</NForm>
<NFormItem :label="t('setting.down_number')" path="DownNumber">
<NInputNumber v-model:value="formValue.DownNumber" :min="1" :max="10"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.down_number_tip") }}
</NTooltip>
</NFormItem>
<NFormItem label="UserAgent" path="UserAgent">
<NInput v-model:value="formValue.UserAgent" placeholder="UserAgent"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.user_agent_tip") }}
</NTooltip>
</NFormItem>
<NFormItem label="Headers" path="Headers">
<NInput v-model:value="formValue.UseHeaders" placeholder="User-Agent,Referer,Authorization,Cookie"/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.use_headers_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.domain_rule')" path="DomainRule">
<NInput
v-model:value="formValue.Rule"
type="textarea"
rows="5"
:placeholder="t('setting.domain_rule_tip')"
/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.domain_rule_tip") }}
</NTooltip>
</NFormItem>
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
<NInput
v-model:value="MimeMap"
type="textarea"
rows="11"
placeholder='{"video/mp4": { "Type": "video","Suffix": ".mp4"}}'
/>
<NTooltip trigger="hover">
<template #trigger>
<NIcon size="18" class="ml-1 text-gray-500">
<HelpCircleOutline/>
</NIcon>
</template>
{{ t("setting.mime_map_tip") }}
</NTooltip>
</NFormItem>
</NForm>
</NTabPane>
</NTabs>
</div>
</template>
@@ -182,6 +243,9 @@ import type {appType} from "@/types/app"
import appApi from "@/api/app"
import {computed} from "vue"
import {useI18n} from 'vue-i18n'
import {isValidHost, isValidPort} from '@/func'
import {NButton, NIcon} from "naive-ui"
import * as bind from "../../wailsjs/go/core/Bind"
const {t} = useI18n()
const store = useIndexStore()
@@ -197,7 +261,26 @@ const formValue = ref<appType.Config>(Object.assign({}, store.globalConfig))
const MimeMap = ref(formValue.value.MimeMap ? JSON.stringify(formValue.value.MimeMap, null, 2) : "")
const renderKey = ref(999)
const hostValidationFeedback = ref("")
const portValidationFeedback = ref("")
watch(formValue.value, () => {
formValue.value.Port = formValue.value.Port.trim()
formValue.value.Host = formValue.value.Host.trim()
if (!isValidHost(formValue.value.Host)) {
hostValidationFeedback.value = t("setting.host_format_error")
return
} else {
hostValidationFeedback.value = ''
}
if (!isValidPort(parseInt(formValue.value.Port))) {
portValidationFeedback.value = t("setting.port_format_error")
return
} else {
portValidationFeedback.value = ''
}
store.setConfig(formValue.value)
}, {deep: true})
@@ -225,6 +308,17 @@ const selectDir = () => {
}
}).catch((err: any) => {
window?.$message?.error(err)
});
})
}
</script>
const resetHandle = ()=>{
localStorage.clear()
bind.ResetApp()
}
</script>
<style lang="scss">
.n-tabs-nav--top{
@apply sticky top-0 z-10;
background-color: var(--n-color);
}
</style>

View File

@@ -5,3 +5,5 @@ import {core} from '../models';
export function AppInfo():Promise<core.ResponseData>;
export function Config():Promise<core.ResponseData>;
export function ResetApp():Promise<void>;

View File

@@ -9,3 +9,7 @@ export function AppInfo() {
export function Config() {
return window['go']['core']['Bind']['Config']();
}
export function ResetApp() {
return window['go']['core']['Bind']['ResetApp']();
}

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.22.0
toolchain go1.23.2
require (
github.com/elazarl/goproxy v0.0.0-20241223171911-d5978cb8c956
github.com/elazarl/goproxy v1.7.2
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/rs/zerolog v1.33.0
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68

5
go.sum
View File

@@ -3,8 +3,8 @@ github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3IS
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20241223171911-d5978cb8c956 h1:HyPt0ZkHkpke+HFl/4dDMz55A/AjFn7ZnLSm8GfdnwU=
github.com/elazarl/goproxy v0.0.0-20241223171911-d5978cb8c956/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -90,4 +90,3 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

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