mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-12 22:34:56 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb6a71f6b6 | ||
|
|
b562f76c69 | ||
|
|
8aaf95fd36 | ||
|
|
983d72d65a | ||
|
|
86378b9fba | ||
|
|
6b18e7fba1 | ||
|
|
ec11132240 | ||
|
|
dc877bd634 | ||
|
|
00b4bf4068 | ||
|
|
51c43564b6 | ||
|
|
820a2671cf | ||
|
|
3b4443110e | ||
|
|
ffd5b29030 | ||
|
|
779f56dd91 | ||
|
|
2beecdade2 |
@@ -14,7 +14,7 @@
|
|||||||
!define INFO_PRODUCTNAME "res-downloader"
|
!define INFO_PRODUCTNAME "res-downloader"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTVERSION
|
!ifndef INFO_PRODUCTVERSION
|
||||||
!define INFO_PRODUCTVERSION "3.1.0"
|
!define INFO_PRODUCTVERSION "3.1.3"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COPYRIGHT
|
!ifndef INFO_COPYRIGHT
|
||||||
!define INFO_COPYRIGHT "Copyright © 2023"
|
!define INFO_COPYRIGHT "Copyright © 2023"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ var (
|
|||||||
systemOnce *SystemSetup
|
systemOnce *SystemSetup
|
||||||
proxyOnce *Proxy
|
proxyOnce *Proxy
|
||||||
httpServerOnce *HttpServer
|
httpServerOnce *HttpServer
|
||||||
|
ruleOnce *RuleSet
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetApp(assets embed.FS, wjs string) *App {
|
func GetApp(assets embed.FS, wjs string) *App {
|
||||||
@@ -120,6 +121,7 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
|
|||||||
initResource()
|
initResource()
|
||||||
initHttpServer()
|
initHttpServer()
|
||||||
initSystem()
|
initSystem()
|
||||||
|
initRule()
|
||||||
}
|
}
|
||||||
return appOnce
|
return appOnce
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type Config struct {
|
|||||||
UseHeaders string `json:"UseHeaders"`
|
UseHeaders string `json:"UseHeaders"`
|
||||||
InsertTail bool `json:"InsertTail"`
|
InsertTail bool `json:"InsertTail"`
|
||||||
MimeMap map[string]MimeInfo `json:"MimeMap"`
|
MimeMap map[string]MimeInfo `json:"MimeMap"`
|
||||||
|
Rule string `json:"Rule"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -65,9 +66,10 @@ func initConfig() *Config {
|
|||||||
TaskNumber: runtime.NumCPU() * 2,
|
TaskNumber: runtime.NumCPU() * 2,
|
||||||
DownNumber: 3,
|
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",
|
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",
|
UseHeaders: "default",
|
||||||
InsertTail: true,
|
InsertTail: true,
|
||||||
MimeMap: getDefaultMimeMap(),
|
MimeMap: getDefaultMimeMap(),
|
||||||
|
Rule: "*",
|
||||||
}
|
}
|
||||||
|
|
||||||
rawDefaults, err := json.Marshal(defaultConfig)
|
rawDefaults, err := json.Marshal(defaultConfig)
|
||||||
@@ -161,6 +163,7 @@ func getDefaultMimeMap() map[string]MimeInfo {
|
|||||||
"application/vnd.apple.mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
"application/vnd.apple.mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||||
"application/x-mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
"application/x-mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||||
"application/x-mpeg": {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/pdf": {Type: "pdf", Suffix: ".pdf"},
|
||||||
"application/vnd.ms-powerpoint": {Type: "ppt", Suffix: ".ppt"},
|
"application/vnd.ms-powerpoint": {Type: "ppt", Suffix: ".ppt"},
|
||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": {Type: "ppt", Suffix: ".pptx"},
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": {Type: "ppt", Suffix: ".pptx"},
|
||||||
@@ -172,7 +175,8 @@ func getDefaultMimeMap() map[string]MimeInfo {
|
|||||||
"text/rtf": {Type: "doc", Suffix: ".rtf"},
|
"text/rtf": {Type: "doc", Suffix: ".rtf"},
|
||||||
"application/vnd.oasis.opendocument.text": {Type: "doc", Suffix: ".odt"},
|
"application/vnd.oasis.opendocument.text": {Type: "doc", Suffix: ".odt"},
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {Type: "doc", Suffix: ".docx"},
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {Type: "doc", Suffix: ".docx"},
|
||||||
"font/woff": {Type: "font", Suffix: ".woff"},
|
"font/woff": {Type: "font", Suffix: ".woff"},
|
||||||
|
"application/octet-stream": {Type: "stream", Suffix: "default"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +209,7 @@ func getDefaultDownloadDir() string {
|
|||||||
func (c *Config) setConfig(config Config) {
|
func (c *Config) setConfig(config Config) {
|
||||||
oldProxy := c.UpstreamProxy
|
oldProxy := c.UpstreamProxy
|
||||||
openProxy := c.OpenProxy
|
openProxy := c.OpenProxy
|
||||||
|
oldRule := c.Rule
|
||||||
c.Host = config.Host
|
c.Host = config.Host
|
||||||
c.Port = config.Port
|
c.Port = config.Port
|
||||||
c.Theme = config.Theme
|
c.Theme = config.Theme
|
||||||
@@ -223,10 +228,18 @@ func (c *Config) setConfig(config Config) {
|
|||||||
c.WxAction = config.WxAction
|
c.WxAction = config.WxAction
|
||||||
c.UseHeaders = config.UseHeaders
|
c.UseHeaders = config.UseHeaders
|
||||||
c.InsertTail = config.InsertTail
|
c.InsertTail = config.InsertTail
|
||||||
|
c.Rule = config.Rule
|
||||||
if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy {
|
if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy {
|
||||||
proxyOnce.setTransport()
|
proxyOnce.setTransport()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if oldRule != c.Rule {
|
||||||
|
err := ruleOnce.Load(c.Rule)
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Esg(err, "set rule failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mimeMux.Lock()
|
mimeMux.Lock()
|
||||||
c.MimeMap = config.MimeMap
|
c.MimeMap = config.MimeMap
|
||||||
mimeMux.Unlock()
|
mimeMux.Unlock()
|
||||||
@@ -279,6 +292,8 @@ func (c *Config) getConfig(key string) interface{} {
|
|||||||
mimeMux.RLock()
|
mimeMux.RLock()
|
||||||
defer mimeMux.RUnlock()
|
defer mimeMux.RUnlock()
|
||||||
return c.MimeMap
|
return c.MimeMap
|
||||||
|
case "Rule":
|
||||||
|
return c.Rule
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,41 @@ func (fd *FileDownloader) buildClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (fd *FileDownloader) setHeaders(request *http.Request) {
|
||||||
for key, value := range fd.Headers {
|
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) {
|
if strings.Contains(globalConfig.UseHeaders, key) {
|
||||||
request.Header.Set(key, value)
|
request.Header.Set(key, value)
|
||||||
}
|
}
|
||||||
|
|||||||
29
core/http.go
29
core/http.go
@@ -84,8 +84,6 @@ func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
|
|||||||
request.Header.Set("Range", rangeHeader)
|
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)
|
resp, err := http.DefaultClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
|
||||||
@@ -93,12 +91,15 @@ func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
for k, v := range resp.Header {
|
||||||
w.WriteHeader(resp.StatusCode)
|
if strings.ToLower(k) == "access-control-allow-origin" {
|
||||||
|
continue
|
||||||
if contentRange := resp.Header.Get("Content-Range"); contentRange != "" {
|
}
|
||||||
w.Header().Set("Content-Range", contentRange)
|
for _, vv := range v {
|
||||||
|
w.Header().Add(k, vv)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
|
||||||
_, err = io.Copy(w, resp.Body)
|
_, err = io.Copy(w, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -323,18 +324,20 @@ func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
|
func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
|
||||||
var data struct {
|
var data struct {
|
||||||
Sign string `json:"sign"`
|
Sign []string `json:"sign"`
|
||||||
}
|
}
|
||||||
err := json.NewDecoder(r.Body).Decode(&data)
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err == nil && data.Sign != "" {
|
if err == nil && len(data.Sign) > 0 {
|
||||||
resourceOnce.delete(data.Sign)
|
for _, v := range data.Sign {
|
||||||
|
resourceOnce.delete(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
h.success(w)
|
h.success(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
|
func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
|
||||||
var data struct {
|
var data struct {
|
||||||
MediaInfo
|
shared.MediaInfo
|
||||||
DecodeStr string `json:"decodeStr"`
|
DecodeStr string `json:"decodeStr"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
@@ -347,7 +350,7 @@ func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
|
func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
|
||||||
var data struct {
|
var data struct {
|
||||||
MediaInfo
|
shared.MediaInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
@@ -365,7 +368,7 @@ func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
|
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
|
||||||
var data struct {
|
var data struct {
|
||||||
MediaInfo
|
shared.MediaInfo
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
DecodeStr string `json:"decodeStr"`
|
DecodeStr string `json:"decodeStr"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ func Middleware(next http.Handler) http.Handler {
|
|||||||
func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if strings.HasPrefix(r.URL.Path, "/api") {
|
if strings.HasPrefix(r.URL.Path, "/api") {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
if r.URL.Path != "/api/preview" {
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
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 {
|
if r.Method == http.MethodOptions {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"github.com/elazarl/goproxy"
|
"github.com/elazarl/goproxy"
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"res-downloader/core/shared"
|
"res-downloader/core/shared"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DefaultPlugin struct {
|
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 {
|
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
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +41,13 @@ func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *
|
|||||||
isAll, _ := p.bridge.GetResType("all")
|
isAll, _ := p.bridge.GetResType("all")
|
||||||
isClassify, _ := p.bridge.GetResType(classify)
|
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)
|
urlSign := shared.Md5(rawUrl)
|
||||||
if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
|
if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
|
||||||
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
|
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,
|
Url: rawUrl,
|
||||||
UrlSign: urlSign,
|
UrlSign: urlSign,
|
||||||
CoverUrl: "",
|
CoverUrl: "",
|
||||||
Size: shared.FormatSize(value),
|
Size: value,
|
||||||
Domain: shared.GetTopLevelDomain(rawUrl),
|
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||||
Classify: classify,
|
Classify: classify,
|
||||||
Suffix: suffix,
|
Suffix: suffix,
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ func (p *QqPlugin) handleMedia(body []byte) {
|
|||||||
Url: rawUrl,
|
Url: rawUrl,
|
||||||
UrlSign: urlSign,
|
UrlSign: urlSign,
|
||||||
CoverUrl: "",
|
CoverUrl: "",
|
||||||
Size: "0",
|
Size: 0,
|
||||||
Domain: shared.GetTopLevelDomain(rawUrl),
|
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||||
Classify: "video",
|
Classify: "video",
|
||||||
Suffix: ".mp4",
|
Suffix: ".mp4",
|
||||||
@@ -201,10 +201,10 @@ func (p *QqPlugin) handleMedia(body []byte) {
|
|||||||
|
|
||||||
switch size := firstMedia["fileSize"].(type) {
|
switch size := firstMedia["fileSize"].(type) {
|
||||||
case float64:
|
case float64:
|
||||||
res.Size = shared.FormatSize(size)
|
res.Size = size
|
||||||
case string:
|
case string:
|
||||||
if value, err := strconv.ParseFloat(size, 64); err == nil {
|
if value, err := strconv.ParseFloat(size, 64); err == nil {
|
||||||
res.Size = shared.FormatSize(value)
|
res.Size = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,23 +21,6 @@ type Proxy struct {
|
|||||||
Is bool
|
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)
|
var pluginRegistry = make(map[string]shared.Plugin)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -97,7 +80,14 @@ func (p *Proxy) Startup() {
|
|||||||
//p.Proxy.KeepDestinationHeaders = true
|
//p.Proxy.KeepDestinationHeaders = true
|
||||||
//p.Proxy.Verbose = false
|
//p.Proxy.Verbose = false
|
||||||
p.setTransport()
|
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.OnRequest().DoFunc(p.httpRequestEvent)
|
||||||
p.Proxy.OnResponse().DoFunc(p.httpResponseEvent)
|
p.Proxy.OnResponse().DoFunc(p.httpResponseEvent)
|
||||||
}
|
}
|
||||||
@@ -130,10 +120,14 @@ func (p *Proxy) setTransport() {
|
|||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.Proxy.ConnectDial = nil
|
||||||
|
p.Proxy.ConnectDialWithReq = nil
|
||||||
|
|
||||||
if globalConfig.UpstreamProxy != "" && globalConfig.OpenProxy && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
|
if globalConfig.UpstreamProxy != "" && globalConfig.OpenProxy && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
|
||||||
proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
|
proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
transport.Proxy = http.ProxyURL(proxyURL)
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
p.Proxy.ConnectDial = p.Proxy.NewConnectDialToProxy(globalConfig.UpstreamProxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.Proxy.Tr = transport
|
p.Proxy.Tr = transport
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ func (r *Resource) cancel(id string) error {
|
|||||||
return errors.New("task not found")
|
return errors.New("task not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
func (r *Resource) download(mediaInfo shared.MediaInfo, decodeStr string) {
|
||||||
if globalConfig.SaveDirectory == "" {
|
if globalConfig.SaveDirectory == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func(mediaInfo MediaInfo) {
|
go func(mediaInfo shared.MediaInfo) {
|
||||||
rawUrl := mediaInfo.Url
|
rawUrl := mediaInfo.Url
|
||||||
fileName := shared.Md5(rawUrl)
|
fileName := shared.Md5(rawUrl)
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
|||||||
}(mediaInfo)
|
}(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)
|
headers := make(map[string]string)
|
||||||
|
|
||||||
if hh, ok := mediaInfo.OtherData["headers"]; ok {
|
if hh, ok := mediaInfo.OtherData["headers"]; ok {
|
||||||
@@ -199,7 +199,7 @@ func (r *Resource) parseHeaders(mediaInfo MediaInfo) (map[string]string, error)
|
|||||||
return headers, nil
|
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)
|
sourceFile, err := os.Open(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -224,7 +224,7 @@ func (r *Resource) wxFileDecode(mediaInfo MediaInfo, fileName, decodeStr string)
|
|||||||
return mediaInfo.SavePath, nil
|
return mediaInfo.SavePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resource) progressEventsEmit(mediaInfo MediaInfo, args ...string) {
|
func (r *Resource) progressEventsEmit(mediaInfo shared.MediaInfo, args ...string) {
|
||||||
Status := shared.DownloadStatusError
|
Status := shared.DownloadStatusError
|
||||||
Message := "ok"
|
Message := "ok"
|
||||||
|
|
||||||
|
|||||||
126
core/rule.go
Normal file
126
core/rule.go
Normal 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
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ type MediaInfo struct {
|
|||||||
Url string
|
Url string
|
||||||
UrlSign string
|
UrlSign string
|
||||||
CoverUrl string
|
CoverUrl string
|
||||||
Size string
|
Size float64
|
||||||
Domain string
|
Domain string
|
||||||
Classify string
|
Classify string
|
||||||
Suffix string
|
Suffix string
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
sysRuntime "runtime"
|
sysRuntime "runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -66,10 +67,41 @@ func IsDevelopment() bool {
|
|||||||
|
|
||||||
func GetFileNameFromURL(rawUrl string) string {
|
func GetFileNameFromURL(rawUrl string) string {
|
||||||
parsedURL, err := url.Parse(rawUrl)
|
parsedURL, err := url.Parse(rawUrl)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return path.Base(parsedURL.Path)
|
return ""
|
||||||
}
|
}
|
||||||
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 {
|
func GetCurrentDateTimeFormatted() string {
|
||||||
|
|||||||
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@@ -15,6 +15,7 @@ declare module 'vue' {
|
|||||||
NaiveProvider: typeof import('./src/components/NaiveProvider.vue')['default']
|
NaiveProvider: typeof import('./src/components/NaiveProvider.vue')['default']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
@@ -48,6 +49,7 @@ declare module 'vue' {
|
|||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
Screen: typeof import('./src/components/Screen.vue')['default']
|
Screen: typeof import('./src/components/Screen.vue')['default']
|
||||||
ShowLoading: typeof import('./src/components/ShowLoading.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']
|
Sider: typeof import('./src/components/layout/Sider.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,12 @@
|
|||||||
#app {
|
#app {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</NIcon>
|
</NIcon>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('cancel')">
|
<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
|
<n-icon
|
||||||
size="28"
|
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"
|
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"
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const playFlvStream = () => {
|
|||||||
try {
|
try {
|
||||||
if (!flvjs.isSupported() || !videoPlayer.value) return
|
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.attachMediaElement(videoPlayer.value)
|
||||||
flvPlayer.load()
|
flvPlayer.load()
|
||||||
flvPlayer.play()
|
flvPlayer.play()
|
||||||
@@ -105,7 +105,7 @@ const setupVideoJsPlayer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.src({
|
player.src({
|
||||||
src: props.previewRow.Url,
|
src: window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(props.previewRow.Url),
|
||||||
type: props.previewRow.ContentType,
|
type: props.previewRow.ContentType,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
@@ -113,7 +113,7 @@ const setupVideoJsPlayer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const playVideoWithoutTotalLength = () => {
|
const playVideoWithoutTotalLength = () => {
|
||||||
rowUrl = buildUrlWithParams(props.previewRow.Url)
|
rowUrl = window?.$baseUrl + "/api/preview?url=" + encodeURIComponent(buildUrlWithParams(props.previewRow.Url))
|
||||||
mediaSource = new MediaSource()
|
mediaSource = new MediaSource()
|
||||||
videoPlayer.value.src = URL.createObjectURL(mediaSource)
|
videoPlayer.value.src = URL.createObjectURL(mediaSource)
|
||||||
videoPlayer.value.play()
|
videoPlayer.value.play()
|
||||||
|
|||||||
59
frontend/src/components/ShowOrEdit.vue
Normal file
59
frontend/src/components/ShowOrEdit.vue
Normal 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>
|
||||||
@@ -26,4 +26,15 @@ export const isValidHost = (host: string) => {
|
|||||||
export const isValidPort = (port: number) => {
|
export const isValidPort = (port: number) => {
|
||||||
const portNumber = Number(port)
|
const portNumber = Number(port)
|
||||||
return Number.isInteger(portNumber) && portNumber > 1024 && portNumber < 65535
|
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';
|
||||||
}
|
}
|
||||||
@@ -34,25 +34,29 @@
|
|||||||
"grab_type": "Grab Type",
|
"grab_type": "Grab Type",
|
||||||
"clear_list": "Clear List",
|
"clear_list": "Clear List",
|
||||||
"clear_list_tip": "Clear all records?",
|
"clear_list_tip": "Clear all records?",
|
||||||
|
"remember_clear_choice": "Remember this selection and clear it next time",
|
||||||
"batch_download": "Batch Download",
|
"batch_download": "Batch Download",
|
||||||
"batch_export": "Batch Export",
|
"batch_export": "Batch Export",
|
||||||
"batch_import": "Batch Import",
|
"batch_import": "Batch Import",
|
||||||
"export_url": "Export Url",
|
"export_url": "Export Url",
|
||||||
"import_success": "Export Success",
|
"import_success": "Export Success",
|
||||||
|
"total_resources": "total of {count} resources",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"m3u8": "M3U8",
|
"m3u8": "M3U8",
|
||||||
"live": "Live Stream",
|
"live": "Live Stream",
|
||||||
|
"stream": "Data Stream",
|
||||||
"xls": "Spreadsheet",
|
"xls": "Spreadsheet",
|
||||||
"doc": "Document",
|
"doc": "Document",
|
||||||
"pdf": "PDF",
|
"pdf": "PDF",
|
||||||
"font": "Font",
|
"font": "Font",
|
||||||
"domain": "Domain",
|
"domain": "Domain",
|
||||||
|
"choice": "choice",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"preview_tip": "Preview not supported",
|
"preview_tip": "Cannot preview",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"resource_size": "Resource Size",
|
"resource_size": "Resource Size",
|
||||||
@@ -60,6 +64,7 @@
|
|||||||
"save_path_empty": "Please set save location",
|
"save_path_empty": "Please set save location",
|
||||||
"operation": "Operation",
|
"operation": "Operation",
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
|
"pending": "Pending",
|
||||||
"running": "Running",
|
"running": "Running",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
@@ -72,6 +77,7 @@
|
|||||||
"open_link": "Open Link",
|
"open_link": "Open Link",
|
||||||
"open_file": "Open File",
|
"open_file": "Open File",
|
||||||
"delete_row": "Delete Row",
|
"delete_row": "Delete Row",
|
||||||
|
"delete_tip": "Running tasks cannot be deleted",
|
||||||
"cancel_down": "Cancel Download",
|
"cancel_down": "Cancel Download",
|
||||||
"more_operation": "More Operations",
|
"more_operation": "More Operations",
|
||||||
"video_decode": "WxDecrypt",
|
"video_decode": "WxDecrypt",
|
||||||
@@ -82,13 +88,14 @@
|
|||||||
"import_placeholder": "When adding multiple items, ensure each line contains only one (each link on a new line)",
|
"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'",
|
"win_install_tip": "For the first time using this software, please right-click and select 'Run as administrator'",
|
||||||
"download_queued": "Download has been added to the queue, current queue length:{count}",
|
"download_queued": "has been added to the queue, current queue length:{count}",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search_description": "Keyword Search...",
|
"search_description": "Keyword Search...",
|
||||||
"start_err_tip": "Error Message",
|
"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_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_positiveText": "Clear cache and restart",
|
||||||
"start_err_negativeText": "Close the software"
|
"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": {
|
"setting": {
|
||||||
"restart_tip": "Keep default if unsure, please restart software after modification",
|
"restart_tip": "Keep default if unsure, please restart software after modification",
|
||||||
@@ -113,9 +120,11 @@
|
|||||||
"connections_tip": "Keep default if unsure, usually CPU cores * 2, for faster downloads",
|
"connections_tip": "Keep default if unsure, usually CPU cores * 2, for faster downloads",
|
||||||
"down_number": "Download Number",
|
"down_number": "Download Number",
|
||||||
"down_number_tip": "Number of downloads executed simultaneously",
|
"down_number_tip": "Number of downloads executed simultaneously",
|
||||||
"use_headers_tip": "Define headers for downloads, comma separated",
|
"use_headers_tip": "Default system filtering, Define headers for downloads, comma separated",
|
||||||
"mime_map": "Intercept Rules",
|
"mime_map": "Intercept Rules",
|
||||||
"mime_map_tip": "JSON format, keep default if unsure, please restart software after modification",
|
"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 rule,supports the following: \n*.qq.com\nvideo.qq.com\nexample.com\n\n# Exclude\n!static.qq.com",
|
||||||
"port_format_error": "port format error",
|
"port_format_error": "port format error",
|
||||||
"host_format_error": "host format error",
|
"host_format_error": "host format error",
|
||||||
"basic_setting": "Basic Setting",
|
"basic_setting": "Basic Setting",
|
||||||
|
|||||||
@@ -34,25 +34,29 @@
|
|||||||
"grab_type": "抓取类型",
|
"grab_type": "抓取类型",
|
||||||
"clear_list": "清空列表",
|
"clear_list": "清空列表",
|
||||||
"clear_list_tip": "清空所有记录?",
|
"clear_list_tip": "清空所有记录?",
|
||||||
|
"remember_clear_choice": "记住此选择,下次直接清除",
|
||||||
"batch_download": "批量下载",
|
"batch_download": "批量下载",
|
||||||
"batch_export": "批量导出",
|
"batch_export": "批量导出",
|
||||||
"batch_import": "批量导入",
|
"batch_import": "批量导入",
|
||||||
"export_url": "导出链接",
|
"export_url": "导出链接",
|
||||||
"import_success": "导出成功",
|
"import_success": "导出成功",
|
||||||
|
"total_resources": "共{count}个资源",
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"image": "图片",
|
"image": "图片",
|
||||||
"audio": "音频",
|
"audio": "音频",
|
||||||
"video": "视频",
|
"video": "视频",
|
||||||
"m3u8": "m3u8",
|
"m3u8": "m3u8",
|
||||||
"live": "直播流",
|
"live": "直播流",
|
||||||
|
"stream": "流数据",
|
||||||
"xls": "表格",
|
"xls": "表格",
|
||||||
"doc": "文档",
|
"doc": "文档",
|
||||||
"pdf": "pdf",
|
"pdf": "pdf",
|
||||||
"font": "字体",
|
"font": "字体",
|
||||||
"domain": "域",
|
"domain": "域",
|
||||||
|
"choice": "已选",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"preview_tip": "暂不支持预览",
|
"preview_tip": "无法预览",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
"resource_size": "资源大小",
|
"resource_size": "资源大小",
|
||||||
@@ -60,6 +64,7 @@
|
|||||||
"save_path_empty": "请设置保存位置",
|
"save_path_empty": "请设置保存位置",
|
||||||
"operation": "操作",
|
"operation": "操作",
|
||||||
"ready": "就绪",
|
"ready": "就绪",
|
||||||
|
"pending": "待处理",
|
||||||
"running": "运行中",
|
"running": "运行中",
|
||||||
"error": "错误",
|
"error": "错误",
|
||||||
"done": "完成",
|
"done": "完成",
|
||||||
@@ -72,6 +77,7 @@
|
|||||||
"open_link": "打开链接",
|
"open_link": "打开链接",
|
||||||
"open_file": "打开文件",
|
"open_file": "打开文件",
|
||||||
"delete_row": "删除记录",
|
"delete_row": "删除记录",
|
||||||
|
"delete_tip": "运行中任务无法删除",
|
||||||
"cancel_down": "取消下载",
|
"cancel_down": "取消下载",
|
||||||
"more_operation": "更多操作",
|
"more_operation": "更多操作",
|
||||||
"video_decode": "视频解密",
|
"video_decode": "视频解密",
|
||||||
@@ -82,13 +88,14 @@
|
|||||||
"import_placeholder": "添加多个时,请确保每行只有一个(每个链接回车换行)",
|
"import_placeholder": "添加多个时,请确保每行只有一个(每个链接回车换行)",
|
||||||
"import_empty": "请输入需要导入的数据",
|
"import_empty": "请输入需要导入的数据",
|
||||||
"win_install_tip": "首次启用本软件,请使用鼠标右键选择以管理员身份运行",
|
"win_install_tip": "首次启用本软件,请使用鼠标右键选择以管理员身份运行",
|
||||||
"download_queued": "下载已加入队列,当前队列长度:{count}",
|
"download_queued": "已加入队列,当前队列长度:{count}",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"search_description": "关键字搜索...",
|
"search_description": "关键字搜索...",
|
||||||
"start_err_tip": "错误提示",
|
"start_err_tip": "错误提示",
|
||||||
"start_err_content": "当前启动过程遇到了问题,是否重置应用?",
|
"start_err_content": "当前启动过程遇到了问题,是否重置应用?",
|
||||||
"start_err_positiveText": "清理缓存并重启",
|
"start_err_positiveText": "清理缓存并重启",
|
||||||
"start_err_negativeText": "关闭软件"
|
"start_err_negativeText": "关闭软件",
|
||||||
|
"reset_app_tip": "此操作会删除已拦截数据以及本应用相关数据,请谨慎操作!"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",
|
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",
|
||||||
@@ -113,9 +120,11 @@
|
|||||||
"connections_tip": "如不清楚请保持默认,通常CPU核心数*2,用于加速下载",
|
"connections_tip": "如不清楚请保持默认,通常CPU核心数*2,用于加速下载",
|
||||||
"down_number": "下载数",
|
"down_number": "下载数",
|
||||||
"down_number_tip": "同时进行的下载数量",
|
"down_number_tip": "同时进行的下载数量",
|
||||||
"use_headers_tip": "定义下载时可使用的header参数,逗号分割",
|
"use_headers_tip": "默认系统过滤,定义下载时可使用的header参数,逗号分割",
|
||||||
"mime_map": "拦截规则",
|
"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 格式错误",
|
"port_format_error": "port 格式错误",
|
||||||
"host_format_error": "host 格式错误",
|
"host_format_error": "host 格式错误",
|
||||||
"basic_setting": "基础设置",
|
"basic_setting": "基础设置",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const useIndexStore = defineStore("index-store", () => {
|
|||||||
UserAgent: "",
|
UserAgent: "",
|
||||||
UseHeaders: "",
|
UseHeaders: "",
|
||||||
InsertTail: true,
|
InsertTail: true,
|
||||||
MimeMap: {}
|
MimeMap: {},
|
||||||
|
Rule: "*"
|
||||||
})
|
})
|
||||||
|
|
||||||
const envInfo = ref({
|
const envInfo = ref({
|
||||||
|
|||||||
3
frontend/src/types/app.d.ts
vendored
3
frontend/src/types/app.d.ts
vendored
@@ -31,6 +31,7 @@ export namespace appType {
|
|||||||
UseHeaders: string
|
UseHeaders: string
|
||||||
InsertTail: boolean
|
InsertTail: boolean
|
||||||
MimeMap: { [key: string]: MimeMap }
|
MimeMap: { [key: string]: MimeMap }
|
||||||
|
Rule: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MediaInfo {
|
interface MediaInfo {
|
||||||
@@ -38,7 +39,7 @@ export namespace appType {
|
|||||||
Url: string
|
Url: string
|
||||||
UrlSign: string
|
UrlSign: string
|
||||||
CoverUrl: string
|
CoverUrl: string
|
||||||
Size: string
|
Size: number
|
||||||
Domain: string
|
Domain: string
|
||||||
Classify: string
|
Classify: string
|
||||||
Suffix: string
|
Suffix: string
|
||||||
|
|||||||
@@ -3,16 +3,28 @@
|
|||||||
<div class="pb-2 z-40" id="header">
|
<div class="pb-2 z-40" id="header">
|
||||||
<NSpace>
|
<NSpace>
|
||||||
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">
|
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">
|
||||||
{{ t("index.close_grab") }}
|
<span class="inline-block w-1.5 h-1.5 bg-red-600 rounded-full mr-1 animate-pulse"></span>
|
||||||
|
{{ t("index.close_grab") }}{{ data.length > 0 ? ` ${t('index.total_resources', {count: data.length})}` : '' }}
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">
|
<NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">
|
||||||
{{ t("index.open_grab") }}
|
{{ t("index.open_grab") }}{{ data.length > 0 ? ` ${t('index.total_resources', {count: data.length})}` : '' }}
|
||||||
</NButton>
|
</NButton>
|
||||||
<NSelect style="min-width: 100px;--wails-draggable:no-drag" :placeholder="t('index.grab_type')" v-model:value="resourcesType" multiple clearable
|
<NSelect style="min-width: 100px;--wails-draggable:no-drag" :placeholder="t('index.grab_type')" v-model:value="resourcesType" multiple clearable
|
||||||
:max-tag-count="3" :options="classify"></NSelect>
|
:max-tag-count="3" :options="classify"></NSelect>
|
||||||
<NButtonGroup style="--wails-draggable:no-drag">
|
<NButtonGroup style="--wails-draggable:no-drag">
|
||||||
|
|
||||||
|
<NButton v-if="rememberChoice" tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<TrashOutline/>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t("index.clear_list") }}
|
||||||
|
</NButton>
|
||||||
<n-popconfirm
|
<n-popconfirm
|
||||||
@positive-click="clear"
|
v-else
|
||||||
|
@positive-click="()=>{rememberChoice=rememberChoiceTmp;clear()}"
|
||||||
|
:show-icon="false"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton tertiary type="error" style="--wails-draggable:no-drag">
|
<NButton tertiary type="error" style="--wails-draggable:no-drag">
|
||||||
@@ -24,7 +36,19 @@
|
|||||||
{{ t("index.clear_list") }}
|
{{ t("index.clear_list") }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
{{ t("index.clear_list_tip") }}
|
<div>
|
||||||
|
<div class="flex flex-row items-center text-red-700 my-2 text-base">
|
||||||
|
<n-icon>
|
||||||
|
<TrashOutline/>
|
||||||
|
</n-icon>
|
||||||
|
<p class="ml-1">{{ t("index.clear_list_tip") }}</p>
|
||||||
|
</div>
|
||||||
|
<NCheckbox
|
||||||
|
v-model:checked="rememberChoiceTmp"
|
||||||
|
>
|
||||||
|
<span class="text-gray-400">{{ t('index.remember_clear_choice') }}</span>
|
||||||
|
</NCheckbox>
|
||||||
|
</div>
|
||||||
</n-popconfirm>
|
</n-popconfirm>
|
||||||
|
|
||||||
<NButton tertiary type="primary" @click.stop="batchDown">
|
<NButton tertiary type="primary" @click.stop="batchDown">
|
||||||
@@ -111,10 +135,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {NButton, NIcon, NImage, NInput, NSpace, NTooltip, NPopover} from "naive-ui"
|
import {NButton, NIcon, NImage, NInput, NSpace, NTooltip, NPopover, NGradientText} from "naive-ui"
|
||||||
import {computed, h, onMounted, ref, watch} from "vue"
|
import {computed, h, onMounted, ref, watch} from "vue"
|
||||||
import type {appType} from "@/types/app"
|
import type {appType} from "@/types/app"
|
||||||
import type {DataTableRowKey, ImageRenderToolbarProps, DataTableFilterState,DataTableBaseColumn} from "naive-ui"
|
import type {DataTableRowKey, ImageRenderToolbarProps, DataTableFilterState, DataTableBaseColumn} from "naive-ui"
|
||||||
import Preview from "@/components/Preview.vue"
|
import Preview from "@/components/Preview.vue"
|
||||||
import ShowLoading from "@/components/ShowLoading.vue"
|
import ShowLoading from "@/components/ShowLoading.vue"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -127,6 +151,7 @@ import ImportJson from "@/components/ImportJson.vue"
|
|||||||
import {useEventStore} from "@/stores/event"
|
import {useEventStore} from "@/stores/event"
|
||||||
import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime"
|
import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime"
|
||||||
import Password from "@/components/Password.vue"
|
import Password from "@/components/Password.vue"
|
||||||
|
import ShowOrEdit from "@/components/ShowOrEdit.vue"
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {
|
import {
|
||||||
DownloadOutline,
|
DownloadOutline,
|
||||||
@@ -136,10 +161,11 @@ import {
|
|||||||
Apps,
|
Apps,
|
||||||
TrashOutline, CloseOutline
|
TrashOutline, CloseOutline
|
||||||
} from "@vicons/ionicons5"
|
} from "@vicons/ionicons5"
|
||||||
import { useDialog } from 'naive-ui'
|
import {useDialog} from 'naive-ui'
|
||||||
import * as bind from "../../wailsjs/go/core/Bind"
|
import * as bind from "../../wailsjs/go/core/Bind"
|
||||||
import {Quit} from "../../wailsjs/runtime"
|
import {Quit} from "../../wailsjs/runtime"
|
||||||
import {DialogOptions} from "naive-ui/es/dialog/src/DialogProvider"
|
import {DialogOptions} from "naive-ui/es/dialog/src/DialogProvider"
|
||||||
|
import {formatSize} from "@/func"
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
const eventStore = useEventStore()
|
const eventStore = useEventStore()
|
||||||
@@ -163,6 +189,10 @@ const filteredData = computed(() => {
|
|||||||
result = result.filter(item => item.Description?.toLowerCase().includes(descriptionSearchValue.value.toLowerCase()))
|
result = result.filter(item => item.Description?.toLowerCase().includes(descriptionSearchValue.value.toLowerCase()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (urlSearchValue.value) {
|
||||||
|
result = result.filter(item => item.Url?.toLowerCase().includes(urlSearchValue.value.toLowerCase()))
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -179,12 +209,14 @@ const classifyAlias: { [key: string]: any } = {
|
|||||||
xls: computed(() => t("index.xls")),
|
xls: computed(() => t("index.xls")),
|
||||||
doc: computed(() => t("index.doc")),
|
doc: computed(() => t("index.doc")),
|
||||||
pdf: computed(() => t("index.pdf")),
|
pdf: computed(() => t("index.pdf")),
|
||||||
|
stream: computed(() => t("index.stream")),
|
||||||
font: computed(() => t("index.font"))
|
font: computed(() => t("index.font"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const dwStatus = computed<any>(() => {
|
const dwStatus = computed<any>(() => {
|
||||||
return {
|
return {
|
||||||
ready: t("index.ready"),
|
ready: t("index.ready"),
|
||||||
|
pending: t("index.pending"),
|
||||||
running: t("index.running"),
|
running: t("index.running"),
|
||||||
error: t("index.error"),
|
error: t("index.error"),
|
||||||
done: t("index.done"),
|
done: t("index.done"),
|
||||||
@@ -204,15 +236,58 @@ const classify = ref([
|
|||||||
])
|
])
|
||||||
|
|
||||||
const descriptionSearchValue = ref("")
|
const descriptionSearchValue = ref("")
|
||||||
|
const urlSearchValue = ref("")
|
||||||
|
const rememberChoice = ref(false)
|
||||||
|
const rememberChoiceTmp = ref(false)
|
||||||
|
|
||||||
const columns = ref<any[]>([
|
const columns = ref<any[]>([
|
||||||
{
|
{
|
||||||
type: "selection",
|
type: "selection",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: computed(() => t("index.domain")),
|
title: () => {
|
||||||
|
if (checkedRowKeysValue.value.length > 0) {
|
||||||
|
return h(NGradientText, {type: "success"}, t("index.choice") + `(${checkedRowKeysValue.value.length})`)
|
||||||
|
}
|
||||||
|
return h('div', {class: 'flex items-center'}, [
|
||||||
|
t('index.domain'),
|
||||||
|
h(NPopover, {
|
||||||
|
style: "--wails-draggable:no-drag",
|
||||||
|
trigger: 'click',
|
||||||
|
placement: 'bottom',
|
||||||
|
showArrow: true,
|
||||||
|
}, {
|
||||||
|
trigger: () => h(NIcon, {
|
||||||
|
size: "18",
|
||||||
|
class: `ml-1 cursor-pointer ${urlSearchValue.value ? "text-green-600": "text-gray-500"}`,
|
||||||
|
onClick: (e: MouseEvent) => e.stopPropagation()
|
||||||
|
}, h(SearchOutline)),
|
||||||
|
default: () => h('div', {class: 'p-2 w-64'}, [
|
||||||
|
h(NInput, {
|
||||||
|
value: urlSearchValue.value,
|
||||||
|
'onUpdate:value': (val: string) => urlSearchValue.value = val,
|
||||||
|
placeholder: t('index.search_description'),
|
||||||
|
clearable: true
|
||||||
|
}, {
|
||||||
|
prefix: () => h(NIcon, {component: SearchOutline})
|
||||||
|
})
|
||||||
|
])
|
||||||
|
})
|
||||||
|
])
|
||||||
|
},
|
||||||
key: "Domain",
|
key: "Domain",
|
||||||
width: 90,
|
width: 90,
|
||||||
|
render: (row: appType.MediaInfo) => {
|
||||||
|
return h(NTooltip, {
|
||||||
|
trigger: 'hover',
|
||||||
|
placement: 'top'
|
||||||
|
}, {
|
||||||
|
trigger: () => h('span', {
|
||||||
|
class: 'cursor-default'
|
||||||
|
}, row.Domain),
|
||||||
|
default: () => row.Url
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: computed(() => t("index.type")),
|
title: computed(() => t("index.type")),
|
||||||
@@ -278,11 +353,18 @@ const columns = ref<any[]>([
|
|||||||
key: "Status",
|
key: "Status",
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (row: appType.MediaInfo, index: number) => {
|
render: (row: appType.MediaInfo, index: number) => {
|
||||||
|
let status = "info"
|
||||||
|
if (row.Status === "done" || row.Status === "running") {
|
||||||
|
status = "success"
|
||||||
|
} else if (row.Status === "pending") {
|
||||||
|
status = "warning"
|
||||||
|
}
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
tertiary: true,
|
tertiary: true,
|
||||||
type: row.Status === "done" ? "success" : "info",
|
type: status as any,
|
||||||
size: "small",
|
size: "small",
|
||||||
style: {
|
style: {
|
||||||
margin: "2px"
|
margin: "2px"
|
||||||
@@ -307,14 +389,14 @@ const columns = ref<any[]>([
|
|||||||
title: () => h('div', {class: 'flex items-center'}, [
|
title: () => h('div', {class: 'flex items-center'}, [
|
||||||
t('index.description'),
|
t('index.description'),
|
||||||
h(NPopover, {
|
h(NPopover, {
|
||||||
style:"--wails-draggable:no-drag",
|
style: "--wails-draggable:no-drag",
|
||||||
trigger: 'click',
|
trigger: 'click',
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
showArrow: true,
|
showArrow: true,
|
||||||
}, {
|
}, {
|
||||||
trigger: () => h(NIcon, {
|
trigger: () => h(NIcon, {
|
||||||
size: "18",
|
size: "18",
|
||||||
class: "ml-1 text-gray-500 cursor-pointer",
|
class: `ml-1 cursor-pointer ${descriptionSearchValue.value ? "text-green-600": "text-gray-500"}`,
|
||||||
onClick: (e: MouseEvent) => e.stopPropagation()
|
onClick: (e: MouseEvent) => e.stopPropagation()
|
||||||
}, h(SearchOutline)),
|
}, h(SearchOutline)),
|
||||||
default: () => h('div', {class: 'p-2 w-64'}, [
|
default: () => h('div', {class: 'p-2 w-64'}, [
|
||||||
@@ -332,10 +414,12 @@ const columns = ref<any[]>([
|
|||||||
key: "Description",
|
key: "Description",
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (row: appType.MediaInfo, index: number) => {
|
render: (row: appType.MediaInfo, index: number) => {
|
||||||
const d = h("div", {class: "ellipsis-2",}, row.Description)
|
return h(ShowOrEdit, {
|
||||||
return h(NTooltip, {trigger: 'hover', placement: 'top'}, {
|
value: row.Description,
|
||||||
trigger: () => d,
|
onUpdateValue(v: string) {
|
||||||
default: () => d
|
data.value[index].Description = v
|
||||||
|
cacheData()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -343,6 +427,10 @@ const columns = ref<any[]>([
|
|||||||
title: computed(() => t("index.resource_size")),
|
title: computed(() => t("index.resource_size")),
|
||||||
key: "Size",
|
key: "Size",
|
||||||
width: 120,
|
width: 120,
|
||||||
|
sorter: (row1: appType.MediaInfo, row2: appType.MediaInfo) => row1.Size - row2.Size,
|
||||||
|
render(row: appType.MediaInfo, index: number) {
|
||||||
|
return formatSize(row.Size)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: computed(() => t("index.save_path")),
|
title: computed(() => t("index.save_path")),
|
||||||
@@ -401,8 +489,8 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
checkLoading()
|
checkLoading()
|
||||||
watch(showPassword, ()=>{
|
watch(showPassword, () => {
|
||||||
if (!showPassword.value){
|
if (!showPassword.value) {
|
||||||
checkLoading()
|
checkLoading()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -423,7 +511,22 @@ onMounted(() => {
|
|||||||
if (cache) {
|
if (cache) {
|
||||||
data.value = JSON.parse(cache)
|
data.value = JSON.parse(cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const choiceCache = localStorage.getItem("remember-clear-choice")
|
||||||
|
if (choiceCache === "1") {
|
||||||
|
rememberChoice.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(rememberChoice, (n, o) => {
|
||||||
|
if (rememberChoice.value) {
|
||||||
|
localStorage.setItem("remember-clear-choice", "1")
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("remember-clear-choice")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
resetTableHeight()
|
resetTableHeight()
|
||||||
|
|
||||||
eventStore.addHandle({
|
eventStore.addHandle({
|
||||||
type: "newResources",
|
type: "newResources",
|
||||||
event: (res: appType.MediaInfo) => {
|
event: (res: appType.MediaInfo) => {
|
||||||
@@ -451,6 +554,9 @@ onMounted(() => {
|
|||||||
item.SavePath = res.SavePath
|
item.SavePath = res.SavePath
|
||||||
item.Status = 'done'
|
item.Status = 'done'
|
||||||
})
|
})
|
||||||
|
if (activeDownloads > 0) {
|
||||||
|
activeDownloads--
|
||||||
|
}
|
||||||
cacheData()
|
cacheData()
|
||||||
checkQueue()
|
checkQueue()
|
||||||
break
|
break
|
||||||
@@ -459,6 +565,9 @@ onMounted(() => {
|
|||||||
item.SavePath = res.Message
|
item.SavePath = res.Message
|
||||||
item.Status = 'error'
|
item.Status = 'error'
|
||||||
})
|
})
|
||||||
|
if (activeDownloads > 0) {
|
||||||
|
activeDownloads--
|
||||||
|
}
|
||||||
cacheData()
|
cacheData()
|
||||||
checkQueue()
|
checkQueue()
|
||||||
break
|
break
|
||||||
@@ -478,7 +587,7 @@ watch(resourcesType, (n, o) => {
|
|||||||
appApi.setType(resourcesType.value)
|
appApi.setType(resourcesType.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateItem = (id: string, updater: (item: any) => void)=>{
|
const updateItem = (id: string, updater: (item: any) => void) => {
|
||||||
const item = data.value.find(i => i.Id === id)
|
const item = data.value.find(i => i.Id === id)
|
||||||
if (item) updater(item)
|
if (item) updater(item)
|
||||||
}
|
}
|
||||||
@@ -524,12 +633,25 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
|||||||
download(row, index)
|
download(row, index)
|
||||||
break
|
break
|
||||||
case "cancel":
|
case "cancel":
|
||||||
if (row.Status === "running") {
|
if (row.Status === "pending") {
|
||||||
appApi.cancel({id: row.Id}).then((res)=>{
|
const queueIndex = downloadQueue.value.findIndex(item => item.Id === row.Id)
|
||||||
|
if (queueIndex !== -1) {
|
||||||
|
downloadQueue.value.splice(queueIndex, 1)
|
||||||
|
}
|
||||||
|
updateItem(row.Id, item => {
|
||||||
|
item.Status = 'ready'
|
||||||
|
item.SavePath = ''
|
||||||
|
})
|
||||||
|
cacheData()
|
||||||
|
} else if (row.Status === "running") {
|
||||||
|
appApi.cancel({id: row.Id}).then((res) => {
|
||||||
updateItem(row.Id, item => {
|
updateItem(row.Id, item => {
|
||||||
item.Status = 'ready'
|
item.Status = 'ready'
|
||||||
item.SavePath = ''
|
item.SavePath = ''
|
||||||
})
|
})
|
||||||
|
if (activeDownloads > 0) {
|
||||||
|
activeDownloads--
|
||||||
|
}
|
||||||
cacheData()
|
cacheData()
|
||||||
checkQueue()
|
checkQueue()
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -564,7 +686,11 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
|||||||
decodeWxFile(row, index)
|
decodeWxFile(row, index)
|
||||||
break
|
break
|
||||||
case "delete":
|
case "delete":
|
||||||
appApi.delete({sign: row.UrlSign}).then(() => {
|
if (row.Status === "pending" || row.Status === "running") {
|
||||||
|
window?.$message?.error(t("index.delete_tip"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appApi.delete({sign: [row.UrlSign]}).then(() => {
|
||||||
data.value.splice(index, 1)
|
data.value.splice(index, 1)
|
||||||
cacheData()
|
cacheData()
|
||||||
})
|
})
|
||||||
@@ -591,7 +717,7 @@ const handleCheck = (rowKeys: DataTableRowKey[]) => {
|
|||||||
checkedRowKeysValue.value = rowKeys
|
checkedRowKeysValue.value = rowKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFilters = (filters: DataTableFilterState, initiatorColumn: DataTableBaseColumn)=>{
|
const updateFilters = (filters: DataTableFilterState, initiatorColumn: DataTableBaseColumn) => {
|
||||||
filterClassify.value = filters.Classify as string[]
|
filterClassify.value = filters.Classify as string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,22 +741,43 @@ const batchDown = async () => {
|
|||||||
checkedRowKeysValue.value = []
|
checkedRowKeysValue.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchCancel = () =>{
|
const batchCancel = async () => {
|
||||||
if (checkedRowKeysValue.value.length <= 0) {
|
if (checkedRowKeysValue.value.length <= 0) {
|
||||||
window?.$message?.error(t("index.use_data"))
|
window?.$message?.error(t("index.use_data"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
loading.value = true
|
||||||
|
const cancelTasks: Promise<any>[] = []
|
||||||
|
data.value.forEach((item, index) => {
|
||||||
|
if (!checkedRowKeysValue.value.includes(item.Id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
data.value.forEach(async (item, index) => {
|
if (item.Status === "pending") {
|
||||||
if (checkedRowKeysValue.value.includes(item.Id) && item.Status === "running") {
|
const queueIndex = downloadQueue.value.findIndex(qItem => qItem.Id === item.Id)
|
||||||
appApi.cancel({id: item.Id})
|
if (queueIndex !== -1) {
|
||||||
data.value[index].Status = 'ready'
|
downloadQueue.value.splice(queueIndex, 1)
|
||||||
data.value[index].SavePath = ''
|
}
|
||||||
|
item.Status = 'ready'
|
||||||
|
item.SavePath = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Status === "running") {
|
||||||
|
if (activeDownloads > 0) {
|
||||||
|
activeDownloads--
|
||||||
|
}
|
||||||
|
cancelTasks.push(appApi.cancel({id: item.Id}).then(() => {
|
||||||
|
item.Status = 'ready'
|
||||||
|
item.SavePath = ''
|
||||||
|
checkQueue()
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
await Promise.allSettled(cancelTasks)
|
||||||
|
loading.value = false
|
||||||
checkedRowKeysValue.value = []
|
checkedRowKeysValue.value = []
|
||||||
cacheData()
|
cacheData()
|
||||||
checkQueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchExport = (type?: string) => {
|
const batchExport = (type?: string) => {
|
||||||
@@ -649,9 +796,9 @@ const batchExport = (type?: string) => {
|
|||||||
|
|
||||||
let jsonData = data.value.filter(item => checkedRowKeysValue.value.includes(item.Id))
|
let jsonData = data.value.filter(item => checkedRowKeysValue.value.includes(item.Id))
|
||||||
|
|
||||||
if (type === "url"){
|
if (type === "url") {
|
||||||
jsonData = jsonData.map(item => item.Url)
|
jsonData = jsonData.map(item => item.Url)
|
||||||
} else{
|
} else {
|
||||||
jsonData = jsonData.map(item => encodeURIComponent(JSON.stringify(item)))
|
jsonData = jsonData.map(item => encodeURIComponent(JSON.stringify(item)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,9 +834,9 @@ const download = (row: appType.MediaInfo, index: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (activeDownloads >= maxConcurrentDownloads.value) {
|
if (activeDownloads >= maxConcurrentDownloads.value) {
|
||||||
|
row.Status = "pending"
|
||||||
downloadQueue.value.push(row)
|
downloadQueue.value.push(row)
|
||||||
window?.$message?.info((row.Description ? `「${row.Description}」` : "")
|
window?.$message?.info(t("index.download_queued", {count: downloadQueue.value.length}))
|
||||||
+ t("index.download_queued", {count: downloadQueue.value.length}))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,9 +854,6 @@ const startDownload = (row: appType.MediaInfo, index: number) => {
|
|||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
window?.$message?.error(res.message)
|
window?.$message?.error(res.message)
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
|
||||||
activeDownloads--
|
|
||||||
checkQueue()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,25 +888,30 @@ const close = () => {
|
|||||||
store.unsetProxy()
|
store.unsetProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clear = () => {
|
const clear = async () => {
|
||||||
|
const newData = [] as any[]
|
||||||
|
const signs: string[] = []
|
||||||
if (checkedRowKeysValue.value.length > 0) {
|
if (checkedRowKeysValue.value.length > 0) {
|
||||||
let newData = [] as any[]
|
|
||||||
data.value.forEach((item, index) => {
|
data.value.forEach((item, index) => {
|
||||||
if (checkedRowKeysValue.value.includes(item.Id)) {
|
if (checkedRowKeysValue.value.includes(item.Id) && item.Status !== "pending" && item.Status !== "running") {
|
||||||
appApi.delete({sign: item.UrlSign})
|
signs.push(item.UrlSign)
|
||||||
} else {
|
} else {
|
||||||
newData.push(item)
|
newData.push(item)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
data.value = newData
|
|
||||||
checkedRowKeysValue.value = []
|
checkedRowKeysValue.value = []
|
||||||
cacheData()
|
} else {
|
||||||
return
|
data.value.forEach((item, index) => {
|
||||||
|
if (item.Status === "pending" || item.Status === "running") {
|
||||||
|
newData.push(item)
|
||||||
|
} else {
|
||||||
|
signs.push(item.UrlSign)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
await appApi.delete({sign: signs})
|
||||||
data.value = []
|
data.value = newData
|
||||||
localStorage.setItem("resources-data", "")
|
cacheData()
|
||||||
appApi.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodeWxFile = (row: appType.MediaInfo, index: number) => {
|
const decodeWxFile = (row: appType.MediaInfo, index: number) => {
|
||||||
@@ -861,8 +1010,8 @@ const handleInstall = async () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkLoading = ()=>{
|
const checkLoading = () => {
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
if (loading.value && !isInstall && !showPassword.value) {
|
if (loading.value && !isInstall && !showPassword.value) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: t("index.start_err_tip"),
|
title: t("index.start_err_tip"),
|
||||||
@@ -883,13 +1032,4 @@ const checkLoading = ()=>{
|
|||||||
}
|
}
|
||||||
}, 6000)
|
}, 6000)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
.ellipsis-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -76,6 +76,17 @@
|
|||||||
{{ t("setting.insert_tail_tip") }}
|
{{ t("setting.insert_tail_tip") }}
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
</NFormItem>
|
</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>
|
</NForm>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
|
|
||||||
@@ -186,6 +197,22 @@
|
|||||||
</NTooltip>
|
</NTooltip>
|
||||||
</NFormItem>
|
</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">
|
<NFormItem :label="t('setting.mime_map')" path="MimeMap">
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="MimeMap"
|
v-model:value="MimeMap"
|
||||||
@@ -217,6 +244,8 @@ import appApi from "@/api/app"
|
|||||||
import {computed} from "vue"
|
import {computed} from "vue"
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {isValidHost, isValidPort} from '@/func'
|
import {isValidHost, isValidPort} from '@/func'
|
||||||
|
import {NButton, NIcon} from "naive-ui"
|
||||||
|
import * as bind from "../../wailsjs/go/core/Bind"
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
const store = useIndexStore()
|
const store = useIndexStore()
|
||||||
@@ -281,6 +310,11 @@ const selectDir = () => {
|
|||||||
window?.$message?.error(err)
|
window?.$message?.error(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetHandle = ()=>{
|
||||||
|
localStorage.clear()
|
||||||
|
bind.ResetApp()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.n-tabs-nav--top{
|
.n-tabs-nav--top{
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.22.0
|
|||||||
toolchain go1.23.2
|
toolchain go1.23.2
|
||||||
|
|
||||||
require (
|
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/matoous/go-nanoid/v2 v2.1.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
|
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
|
||||||
|
|||||||
5
go.sum
5
go.sum
@@ -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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
github.com/elazarl/goproxy v0.0.0-20241223171911-d5978cb8c956/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
|
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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
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=
|
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=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"companyName": "res-downloader",
|
"companyName": "res-downloader",
|
||||||
"productName": "res-downloader",
|
"productName": "res-downloader",
|
||||||
"productVersion": "3.1.1",
|
"productVersion": "3.1.3",
|
||||||
"copyright": "Copyright © 2023",
|
"copyright": "Copyright © 2023",
|
||||||
"comments": "This is a high-value high-performance and diverse resource downloader called res-downloader."
|
"comments": "This is a high-value high-performance and diverse resource downloader called res-downloader."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user