15 Commits
3.1.0 ... 3.1.2

Author SHA1 Message Date
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
28 changed files with 547 additions and 108 deletions

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.1.0"
!define INFO_PRODUCTVERSION "3.1.1"
!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 (
@@ -51,6 +53,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
@@ -129,6 +132,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 +186,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

@@ -1,12 +1,14 @@
package core
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"res-downloader/core/shared"
"strings"
"sync"
"time"
@@ -47,9 +49,12 @@ type FileDownloader struct {
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,
@@ -59,6 +64,8 @@ func NewFileDownloader(url, filename string, totalTasks int, headers map[string]
TotalSize: 0,
Headers: headers,
DownloadTaskList: make([]*DownloadTask, 0),
ctx: ctx,
cancelFunc: cancelFunc,
}
}
@@ -72,7 +79,6 @@ func (fd *FileDownloader) buildClient() *http.Client {
}
return &http.Client{
Transport: transport,
Timeout: 60 * time.Second,
}
}
@@ -143,6 +149,9 @@ 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)
@@ -268,11 +277,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):
}
}
}
@@ -280,7 +299,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)
}
@@ -307,6 +332,12 @@ 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 {
writeSize := int64(n)
@@ -364,3 +395,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

@@ -84,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)
@@ -93,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 {
@@ -334,7 +335,7 @@ func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
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 {
@@ -345,9 +346,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"`
}

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,6 +58,8 @@ 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-export":

View File

@@ -51,7 +51,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

@@ -53,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
}
@@ -163,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",
@@ -198,10 +201,10 @@ func (p *QqPlugin) handleMedia(body []byte) {
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() {

View File

@@ -3,6 +3,7 @@ package core
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
@@ -22,6 +23,7 @@ type WxFileDecodeResult struct {
type Resource struct {
mediaMark sync.Map
tasks sync.Map
resType map[string]bool
resTypeMux sync.RWMutex
}
@@ -86,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
@@ -107,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") {
@@ -140,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 != "" {
@@ -156,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 {
@@ -175,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
@@ -200,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"

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

@@ -9,7 +9,11 @@ import (
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
sysRuntime "runtime"
"strings"
"time"
)
@@ -61,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",
@@ -72,6 +115,24 @@ func GetCurrentDateTimeFormatted() string {
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

View File

@@ -15,6 +15,7 @@ declare module 'vue' {
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']

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',

View File

@@ -23,6 +23,16 @@
</NIcon>
</template>
<div class="flex flex-col">
<div class="flex items-center justify-start p-1.5 cursor-pointer" v-if="row.Status === 'running'" @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"
@@ -76,6 +86,7 @@ import {
LockOpenSharp,
LinkOutline,
GridSharp,
CloseOutline,
TrashOutline
} from "@vicons/ionicons5"

View File

@@ -18,6 +18,14 @@
<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"
@@ -91,6 +99,7 @@ import {
LinkOutline,
LockOpenSharp,
GridSharp,
CloseOutline,
TrashOutline
} from "@vicons/ionicons5"

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()

View File

@@ -26,4 +26,15 @@ export const isValidHost = (host: string) => {
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

@@ -34,10 +34,13 @@
"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",
@@ -49,6 +52,7 @@
"pdf": "PDF",
"font": "Font",
"domain": "Domain",
"choice": "choice",
"type": "Type",
"preview": "Preview",
"preview_tip": "Preview not supported",
@@ -59,6 +63,7 @@
"save_path_empty": "Please set save location",
"operation": "Operation",
"ready": "Ready",
"pending": "Pending",
"running": "Running",
"error": "Error",
"done": "Done",
@@ -71,6 +76,7 @@
"open_link": "Open Link",
"open_file": "Open File",
"delete_row": "Delete Row",
"cancel_down": "Cancel Download",
"more_operation": "More Operations",
"video_decode": "WxDecrypt",
"video_decode_loading": "Decrypting",
@@ -80,9 +86,14 @@
"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",
"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_description": "Keyword 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",
@@ -96,7 +107,7 @@
"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": "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",

View File

@@ -34,10 +34,13 @@
"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": "音频",
@@ -49,6 +52,7 @@
"pdf": "pdf",
"font": "字体",
"domain": "域",
"choice": "已选",
"type": "类型",
"preview": "预览",
"preview_tip": "暂不支持预览",
@@ -59,6 +63,7 @@
"save_path_empty": "请设置保存位置",
"operation": "操作",
"ready": "就绪",
"pending": "待处理",
"running": "运行中",
"error": "错误",
"done": "完成",
@@ -71,6 +76,7 @@
"open_link": "打开链接",
"open_file": "打开文件",
"delete_row": "删除记录",
"cancel_down": "取消下载",
"more_operation": "更多操作",
"video_decode": "视频解密",
"video_decode_loading": "解密中",
@@ -80,9 +86,14 @@
"import_placeholder": "添加多个时,请确保每行只有一个(每个链接回车换行)",
"import_empty": "请输入需要导入的数据",
"win_install_tip": "首次启用本软件,请使用鼠标右键选择以管理员身份运行",
"download_queued": "下载已加入队列,当前队列长度:{count}",
"download_queued": "已加入队列,当前队列长度:{count}",
"search": "搜索",
"search_description": "关键字搜索..."
"search_description": "关键字搜索...",
"start_err_tip": "错误提示",
"start_err_content": "当前启动过程遇到了问题,是否重置应用?",
"start_err_positiveText": "清理缓存并重启",
"start_err_negativeText": "关闭软件",
"reset_app_tip": "此操作会删除已拦截数据以及本应用相关数据,请谨慎操作!"
},
"setting": {
"restart_tip": "如果不清楚保持默认就行,修改后请重启软件",

View File

@@ -38,7 +38,7 @@ export namespace appType {
Url: string
UrlSign: string
CoverUrl: string
Size: string
Size: number
Domain: string
Classify: string
Suffix: string

View File

@@ -3,29 +3,54 @@
<div class="pb-2 z-40" id="header">
<NSpace>
<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 ? `&nbsp;${t('index.total_resources', {count: data.length})}` : '' }}
</NButton>
<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 ? `&nbsp;${t('index.total_resources', {count: data.length})}` : '' }}
</NButton>
<NSelect style="min-width: 100px;--wails-draggable:no-drag" :placeholder="t('index.grab_type')" v-model:value="resourcesType" multiple clearable
:max-tag-count="3" :options="classify"></NSelect>
<n-popconfirm
@positive-click="clear"
>
<template #trigger>
<NButton tertiary type="error" style="--wails-draggable:no-drag">
<template #icon>
<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
v-else
@positive-click="()=>{rememberChoice=rememberChoiceTmp;clear()}"
:show-icon="false"
>
<template #trigger>
<NButton tertiary type="error" style="--wails-draggable:no-drag">
<template #icon>
<n-icon>
<TrashOutline/>
</n-icon>
</template>
{{ t("index.clear_list") }}
</NButton>
</template>
<div>
<div class="flex flex-row items-center text-red-700 my-2 text-base">
<n-icon>
<TrashOutline/>
</n-icon>
</template>
{{ t("index.clear_list") }}
</NButton>
</template>
{{ t("index.clear_list_tip") }}
</n-popconfirm>
<NButtonGroup style="--wails-draggable:no-drag">
<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>
<NButton tertiary type="primary" @click.stop="batchDown">
<template #icon>
<n-icon>
@@ -34,21 +59,48 @@
</template>
{{ t('index.batch_download') }}
</NButton>
<NButton tertiary type="warning" @click.stop="batchExport">
<template #icon>
<n-icon>
<ArrowRedoCircleOutline/>
</n-icon>
</template>
{{ t('index.batch_export') }}
</NButton>
<NButton tertiary type="info" @click.stop="showImport=true">
<template #icon>
<n-icon>
<ServerOutline/>
</n-icon>
</template>
{{ t('index.batch_import') }}
<NButton tertiary type="info">
<NPopover placement="bottom" trigger="hover">
<template #trigger>
<NIcon size="18" class="">
<Apps/>
</NIcon>
</template>
<div class="flex flex-col">
<NButton tertiary type="error" @click.stop="batchCancel" class="my-1">
<template #icon>
<n-icon>
<CloseOutline/>
</n-icon>
</template>
{{ t('index.cancel_down') }}
</NButton>
<NButton tertiary type="warning" @click.stop="batchExport()" class="my-1">
<template #icon>
<n-icon>
<ArrowRedoCircleOutline/>
</n-icon>
</template>
{{ t('index.batch_export') }}
</NButton>
<NButton tertiary type="info" @click.stop="showImport=true" class="my-1">
<template #icon>
<n-icon>
<ServerOutline/>
</n-icon>
</template>
{{ t('index.batch_import') }}
</NButton>
<NButton tertiary type="primary" @click.stop="batchExport('url')" class="my-1">
<template #icon>
<n-icon>
<ArrowRedoCircleOutline/>
</n-icon>
</template>
{{ t('index.export_url') }}
</NButton>
</div>
</NPopover>
</NButton>
</NButtonGroup>
</NSpace>
@@ -65,6 +117,7 @@
:height-for-row="()=> 48"
:checked-row-keys="checkedRowKeysValue"
@update:checked-row-keys="handleCheck"
@update:filters="updateFilters"
style="--wails-draggable:no-drag"
/>
</div>
@@ -82,10 +135,10 @@
</template>
<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 type {appType} from "@/types/app"
import type {DataTableRowKey, ImageRenderToolbarProps} from "naive-ui"
import type {DataTableRowKey, ImageRenderToolbarProps, DataTableFilterState, DataTableBaseColumn} from "naive-ui"
import Preview from "@/components/Preview.vue"
import ShowLoading from "@/components/ShowLoading.vue"
// @ts-ignore
@@ -104,11 +157,18 @@ import {
ArrowRedoCircleOutline,
ServerOutline,
SearchOutline,
TrashOutline
Apps,
TrashOutline, CloseOutline
} from "@vicons/ionicons5"
import {useDialog} from 'naive-ui'
import * as bind from "../../wailsjs/go/core/Bind"
import {Quit} from "../../wailsjs/runtime"
import {DialogOptions} from "naive-ui/es/dialog/src/DialogProvider"
import {formatSize} from "@/func"
const {t} = useI18n()
const eventStore = useEventStore()
const dialog = useDialog()
const isProxy = computed(() => {
return store.isProxy
})
@@ -116,12 +176,12 @@ const certUrl = computed(() => {
return store.baseUrl + "/api/cert"
})
const data = ref<any[]>([])
let filterClassify: string[] = []
const filterClassify = ref<string[]>([])
const filteredData = computed(() => {
let result = data.value
if (filterClassify.length > 0) {
result = result.filter(item => filterClassify.includes(item.Classify))
if (filterClassify.value.length > 0) {
result = result.filter(item => filterClassify.value.includes(item.Classify))
}
if (descriptionSearchValue.value) {
@@ -150,6 +210,7 @@ const classifyAlias: { [key: string]: any } = {
const dwStatus = computed<any>(() => {
return {
ready: t("index.ready"),
pending: t("index.pending"),
running: t("index.running"),
error: t("index.error"),
done: t("index.done"),
@@ -169,13 +230,17 @@ const classify = ref([
])
const descriptionSearchValue = ref("")
const rememberChoice = ref(false)
const rememberChoiceTmp = ref(false)
const columns = ref<any[]>([
{
type: "selection",
},
{
title: computed(() => t("index.domain")),
title: computed(() => {
return checkedRowKeysValue.value.length > 0 ? h(NGradientText, {type: "success"}, t("index.choice") + `(${checkedRowKeysValue.value.length})`) : t("index.domain")
}),
key: "Domain",
width: 90,
},
@@ -186,7 +251,6 @@ const columns = ref<any[]>([
filterOptions: computed(() => Array.from(classify.value).slice(1)),
filterMultiple: true,
filter: (value: string, row: appType.MediaInfo) => {
if (!filterClassify.includes(value)) filterClassify.push(value)
return !!~row.Classify.indexOf(String(value))
},
render: (row: appType.MediaInfo) => {
@@ -244,11 +308,18 @@ const columns = ref<any[]>([
key: "Status",
width: 80,
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(
NButton,
{
tertiary: true,
type: row.Status === "done" ? "success" : "info",
type: status as any,
size: "small",
style: {
margin: "2px"
@@ -273,6 +344,7 @@ const columns = ref<any[]>([
title: () => h('div', {class: 'flex items-center'}, [
t('index.description'),
h(NPopover, {
style: "--wails-draggable:no-drag",
trigger: 'click',
placement: 'bottom',
showArrow: true,
@@ -308,6 +380,10 @@ const columns = ref<any[]>([
title: computed(() => t("index.resource_size")),
key: "Size",
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")),
@@ -352,6 +428,7 @@ const showPassword = ref(false)
const downloadQueue = ref<appType.MediaInfo[]>([])
let activeDownloads = 0
let isOpenProxy = false
let isInstall = false
onMounted(() => {
try {
@@ -360,8 +437,16 @@ onMounted(() => {
})
loading.value = true
handleInstall().then((is: boolean) => {
isInstall = true
loading.value = false
})
checkLoading()
watch(showPassword, () => {
if (!showPassword.value) {
checkLoading()
}
})
} catch (e) {
window.$message?.error(JSON.stringify(e), {duration: 5000})
}
@@ -379,7 +464,22 @@ onMounted(() => {
if (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()
eventStore.addHandle({
type: "newResources",
event: (res: appType.MediaInfo) => {
@@ -407,6 +507,9 @@ onMounted(() => {
item.SavePath = res.SavePath
item.Status = 'done'
})
if (activeDownloads > 0) {
activeDownloads--
}
cacheData()
checkQueue()
break
@@ -415,6 +518,9 @@ onMounted(() => {
item.SavePath = res.Message
item.Status = 'error'
})
if (activeDownloads > 0) {
activeDownloads--
}
cacheData()
checkQueue()
break
@@ -434,7 +540,7 @@ watch(resourcesType, (n, o) => {
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)
if (item) updater(item)
}
@@ -479,6 +585,25 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
case "down":
download(row, index)
break
case "cancel":
if (row.Status === "running") {
appApi.cancel({id: row.Id}).then((res) => {
updateItem(row.Id, item => {
item.Status = 'ready'
item.SavePath = ''
})
if (activeDownloads > 0) {
activeDownloads--
}
cacheData()
checkQueue()
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
})
}
break
case "copy":
ClipboardSetText(row.Url).then((is: boolean) => {
if (is) {
@@ -531,6 +656,10 @@ const handleCheck = (rowKeys: DataTableRowKey[]) => {
checkedRowKeysValue.value = rowKeys
}
const updateFilters = (filters: DataTableFilterState, initiatorColumn: DataTableBaseColumn) => {
filterClassify.value = filters.Classify as string[]
}
const batchDown = async () => {
if (checkedRowKeysValue.value.length <= 0) {
window?.$message?.error(t("index.use_data"))
@@ -551,7 +680,32 @@ const batchDown = async () => {
checkedRowKeysValue.value = []
}
const batchExport = () => {
const batchCancel = async () => {
if (checkedRowKeysValue.value.length <= 0) {
window?.$message?.error(t("index.use_data"))
return
}
loading.value = true
const cancelTasks: Promise<any>[] = []
data.value.forEach((item, index) => {
if (checkedRowKeysValue.value.includes(item.Id) && 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 = []
cacheData()
}
const batchExport = (type?: string) => {
if (checkedRowKeysValue.value.length <= 0) {
window?.$message?.error(t("index.use_data"))
return
@@ -565,9 +719,13 @@ const batchExport = () => {
loadingText.value = t("common.loading")
loading.value = true
const jsonData = data.value
.filter(item => checkedRowKeysValue.value.includes(item.Id))
.map(item => encodeURIComponent(JSON.stringify(item)))
let jsonData = data.value.filter(item => checkedRowKeysValue.value.includes(item.Id))
if (type === "url") {
jsonData = jsonData.map(item => item.Url)
} else {
jsonData = jsonData.map(item => encodeURIComponent(JSON.stringify(item)))
}
appApi.batchExport({content: jsonData.join("\n")}).then((res: appType.Res) => {
loading.value = false
@@ -601,9 +759,9 @@ const download = (row: appType.MediaInfo, index: number) => {
}
if (activeDownloads >= maxConcurrentDownloads.value) {
row.Status = "pending"
downloadQueue.value.push(row)
window?.$message?.info((row.Description ? `「${row.Description}」` : "")
+ t("index.download_queued", {count: downloadQueue.value.length}))
window?.$message?.info(t("index.download_queued", {count: downloadQueue.value.length}))
return
}
@@ -621,9 +779,6 @@ const startDownload = (row: appType.MediaInfo, index: number) => {
if (res.code === 0) {
window?.$message?.error(res.message)
}
}).finally(() => {
activeDownloads--
checkQueue()
})
}
@@ -732,8 +887,8 @@ const handleImport = (content: string) => {
})
if (newItems.length > 0) {
data.value = [...newItems, ...data.value]
cacheData()
}
cacheData()
showImport.value = false
}
@@ -774,6 +929,29 @@ const handleInstall = async () => {
}
return false
}
const checkLoading = () => {
setTimeout(() => {
if (loading.value && !isInstall && !showPassword.value) {
dialog.warning({
title: t("index.start_err_tip"),
content: t("index.start_err_content"),
positiveText: t("index.start_err_positiveText"),
negativeText: t("index.start_err_negativeText"),
draggable: false,
closeOnEsc: false,
closable: false,
maskClosable: false,
onPositiveClick: () => {
bind.ResetApp()
},
onNegativeClick: () => {
Quit()
}
} as DialogOptions)
}
}, 6000)
}
</script>
<style>
.ellipsis-2 {

View File

@@ -76,6 +76,17 @@
{{ t("setting.insert_tail_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>
@@ -217,6 +228,8 @@ 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()
@@ -281,6 +294,11 @@ const selectDir = () => {
window?.$message?.error(err)
})
}
const resetHandle = ()=>{
localStorage.clear()
bind.ResetApp()
}
</script>
<style lang="scss">
.n-tabs-nav--top{

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']();
}

View File

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