mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5cd53f94c | ||
|
|
847e88881d | ||
|
|
3d20e818e5 | ||
|
|
09f7c88ed3 | ||
|
|
f7b26753c1 | ||
|
|
9704d4af27 | ||
|
|
1907f404ac | ||
|
|
72c5ba7d20 |
No files matched your search
+1
-1
@@ -12,4 +12,4 @@
|
||||
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
# github: monkeyWie
|
||||
ko_fi: gopeed
|
||||
custom: https://docs.gopeed.com/donate.html
|
||||
custom: https://gopeed.com/docs/donate
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
channel: master
|
||||
- run: |
|
||||
sudo apt update -y
|
||||
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev rpm patchelf libfuse2 locate imagemagick potrace libkeybinder-3.0-dev
|
||||
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev rpm patchelf libfuse2 locate libkeybinder-3.0-dev
|
||||
arch=$(uname -m)
|
||||
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||
chmod +x appimagetool
|
||||
@@ -339,18 +339,11 @@ jobs:
|
||||
|
||||
cd ui/flutter
|
||||
|
||||
ICON_PATH=$(find assets -name "logo.png" -o -name "icon.png" | head -n 1)
|
||||
[ -z "$ICON_PATH" ] && ICON_PATH="assets/icon.png" && touch assets/icon.png
|
||||
|
||||
convert "$ICON_PATH" assets/icon.svg
|
||||
ICON_PATH="assets/icon.svg"
|
||||
|
||||
RPM_ARCH=$(uname -m)
|
||||
|
||||
mkdir -p linux/packaging/rpm
|
||||
cat << EOF > linux/packaging/rpm/make_config.yaml
|
||||
display_name: Gopeed
|
||||
icon: $ICON_PATH
|
||||
icon: assets/icon/icon.svg
|
||||
summary: High speed downloader.
|
||||
description: A high speed downloader that supports all platforms.
|
||||
group: Applications/Internet
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
## 🚀 Introduction
|
||||
|
||||
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
|
||||
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet, ED2K) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://gopeed.com/docs/dev-api) or installation and development of [extensions](https://gopeed.com/docs/dev-extension).
|
||||
|
||||
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
|
||||
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://gopeed.com/docs)
|
||||
|
||||
## ⬇️ Download
|
||||
|
||||
@@ -165,7 +165,7 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||
More about installation, please refer to [Installation](https://gopeed.com/docs/install)
|
||||
|
||||
### 🛠️ Command tool
|
||||
|
||||
@@ -187,7 +187,7 @@ Follow our WeChat Official Account to get the latest updates and news.
|
||||
|
||||
## 💝 Donate
|
||||
|
||||
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
|
||||
If you like this project, please consider [donating](https://gopeed.com/docs/donate) to support the development of this project, thank you!
|
||||
|
||||
## 🖼️ Showcase
|
||||
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
## 🚀 はじめに
|
||||
|
||||
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
|
||||
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet、ED2K) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://gopeed.com/docs/dev-api)との連動や[拡張機能](https://gopeed.com/docs/dev-extension)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
|
||||
|
||||
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
|
||||
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://gopeed.com/docs)
|
||||
|
||||
## ⬇️ インストール
|
||||
|
||||
@@ -156,7 +156,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||
インストールについての詳細は、[インストール](https://gopeed.com/docs/install)を参照してください。
|
||||
|
||||
### 🛠️ コマンドツール
|
||||
|
||||
@@ -168,7 +168,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
||||
|
||||
## 💝 寄付
|
||||
|
||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://gopeed.com/docs/donate)をご検討ください!
|
||||
|
||||
## 🖼️ ショーケース
|
||||
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
## 🚀 Giới thiệu
|
||||
|
||||
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
|
||||
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet, ED2K) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://gopeed.com/docs/dev-api) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://gopeed.com/docs/dev-extension).
|
||||
|
||||
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
|
||||
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://gopeed.com/docs)
|
||||
|
||||
## ⬇️ Tải về
|
||||
|
||||
@@ -156,7 +156,7 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
|
||||
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://gopeed.com/docs/install)
|
||||
|
||||
### 🛠️ Công cụ lệnh
|
||||
|
||||
@@ -174,7 +174,7 @@ Theo dõi tài khoản chính thức để nhận các cập nhật và tin tứ
|
||||
|
||||
## 💝 Quyên góp
|
||||
|
||||
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
||||
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://gopeed.com/docs/donate) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
||||
|
||||
## 🖼️ Trưng bày
|
||||
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://docs.gopeed.com/zh/donate.html)
|
||||
[](https://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
## 🚀 介绍
|
||||
|
||||
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
|
||||
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet、ED2K)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://gopeed.com/docs/dev-api)或者安装和开发[扩展](https://gopeed.com/docs/dev-extension)来实现更多的功能。
|
||||
|
||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
|
||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://gopeed.com/docs)
|
||||
|
||||
## ⬇️ 下载
|
||||
|
||||
@@ -156,7 +156,7 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||
更多关于安装的内容请参考[安装文档](https://gopeed.com/docs/install)
|
||||
|
||||
### 🛠️ 命令行工具
|
||||
|
||||
@@ -178,7 +178,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
|
||||
|
||||
## 💝 赞助
|
||||
|
||||
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
||||
如果觉得项目对你有帮助,请考虑[赞助](https://gopeed.com/docs/donate)以支持这个项目的发展,非常感谢!
|
||||
|
||||
## 🖼️ 界面展示
|
||||
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
## 🚀 簡介
|
||||
|
||||
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
|
||||
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet、ED2K)協定,同時支援所有的平台。
|
||||
|
||||
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
||||
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://gopeed.com/docs)
|
||||
|
||||
## ⬇️ 下載
|
||||
|
||||
@@ -157,7 +157,7 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||
更多關於安裝的內容請參考[安裝文檔](https://gopeed.com/docs/install)
|
||||
|
||||
### 🛠️ 使用 CLI 安裝
|
||||
|
||||
@@ -175,7 +175,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
|
||||
## 💝 贊助
|
||||
|
||||
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||
如果你認為該項目對你有所幫助,請考慮[贊助](https://gopeed.com/docs/donate)以支持該項目的持續發展,謝謝!
|
||||
|
||||
## 🖼️ 軟體介面
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ require (
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
|
||||
@@ -386,6 +386,10 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5 h1:tEOucyKJzFsCz5Gr41jFHj8i2g1zemTyS4uyErJgFHc=
|
||||
github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg=
|
||||
github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 h1:88kRsNwkDKPA4NzRUWdOoIL7SyMNDwtmAxzShKBvKTI=
|
||||
github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
|
||||
@@ -127,6 +127,21 @@ type FetcherManager interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// StatefulFetcherManager is an optional extension for protocols that keep
|
||||
// shared client state outside individual task fetchers.
|
||||
type StatefulFetcherManager interface {
|
||||
SetStateStore(store ProtocolStateStore)
|
||||
}
|
||||
|
||||
// ProtocolStateStore persists shared protocol state for a fetcher manager.
|
||||
// Downloader provides the concrete storage backend, while the protocol decides
|
||||
// when state should be loaded or flushed.
|
||||
type ProtocolStateStore interface {
|
||||
Load(v any) (bool, error)
|
||||
Save(v any) error
|
||||
Delete() error
|
||||
}
|
||||
|
||||
type DefaultFetcher struct {
|
||||
Ctl *controller.Controller
|
||||
Meta *FetcherMeta
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package ed2k
|
||||
|
||||
const (
|
||||
defaultServerList = "45.82.80.155:5687,176.123.5.89:4725,85.121.5.137:4232,176.123.2.239:4232,145.239.2.134:4661,91.208.162.87:4232,37.15.61.236:4232"
|
||||
defaultServerMet = "ed2k://|serverlist|http://upd.emule-security.org/server.met|/"
|
||||
defaultNodesDat = "https://upd.emule-security.org/nodes.dat"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ListenPort int `json:"listenPort"`
|
||||
UDPPort int `json:"udpPort"`
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
ServerMet string `json:"serverMet"`
|
||||
NodesDat string `json:"nodesDat"`
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
package ed2k
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
ped2k "github.com/GopeedLab/gopeed/pkg/protocol/ed2k"
|
||||
"github.com/monkeyWie/goed2k"
|
||||
gprotocol "github.com/monkeyWie/goed2k/protocol"
|
||||
)
|
||||
|
||||
type clientStateStore struct {
|
||||
store fetcher.ProtocolStateStore
|
||||
}
|
||||
|
||||
func (s *clientStateStore) Load() (*goed2k.ClientState, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var state goed2k.ClientState
|
||||
exist, err := s.store.Load(&state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, nil
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (s *clientStateStore) Save(state *goed2k.ClientState) error {
|
||||
if s == nil || s.store == nil {
|
||||
return nil
|
||||
}
|
||||
if state == nil {
|
||||
return s.store.Delete()
|
||||
}
|
||||
return s.store.Save(state)
|
||||
}
|
||||
|
||||
type Fetcher struct {
|
||||
ctl *controller.Controller
|
||||
config *config
|
||||
|
||||
manager *FetcherManager
|
||||
meta *fetcher.FetcherMeta
|
||||
handle goed2k.TransferHandle
|
||||
|
||||
waitCtx context.Context
|
||||
waitCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
f.ctl = ctl
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
f.waitCtx, f.waitCancel = context.WithCancel(context.Background())
|
||||
f.ctl.GetConfig(&f.config)
|
||||
}
|
||||
|
||||
func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) error {
|
||||
link, err := parseLink(req.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.meta.Req = req
|
||||
f.meta.Opts = opts
|
||||
if f.meta.Opts == nil {
|
||||
f.meta.Opts = &base.Options{}
|
||||
}
|
||||
f.meta.Res = buildResource(link)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Start() error {
|
||||
link, err := parseLink(f.meta.Req.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.meta.Res == nil {
|
||||
f.meta.Res = buildResource(link)
|
||||
}
|
||||
|
||||
client, err := f.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := f.meta.SingleFilepath()
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handle := client.FindTransfer(link.Hash)
|
||||
if handle.IsValid() {
|
||||
f.handle = handle
|
||||
if handle.IsPaused() {
|
||||
if err := client.ResumeTransfer(handle.GetHash()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
atp := goed2k.AddTransferParams{
|
||||
Hash: link.Hash,
|
||||
CreateTime: time.Now().UnixMilli(),
|
||||
Size: link.NumberValue,
|
||||
FilePath: targetPath,
|
||||
}
|
||||
handle, err = client.AddTransfer(atp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.handle = handle
|
||||
if handle.IsValid() && handle.IsPaused() {
|
||||
if err := client.ResumeTransfer(handle.GetHash()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Patch(req *base.Request, opts *base.Options) error {
|
||||
handle := f.currentHandle()
|
||||
|
||||
if opts != nil && (opts.Name != "" || opts.Path != "") && handle.IsValid() {
|
||||
return errors.New("cannot change ed2k target path after transfer started")
|
||||
}
|
||||
if req != nil && req.URL != "" && handle.IsValid() {
|
||||
return errors.New("cannot change ed2k link after transfer started")
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
if req.URL != "" {
|
||||
link, err := parseLink(req.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.meta.Req.URL = req.URL
|
||||
f.meta.Res = buildResource(link)
|
||||
}
|
||||
if req.Labels != nil {
|
||||
if f.meta.Req.Labels == nil {
|
||||
f.meta.Req.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range req.Labels {
|
||||
f.meta.Req.Labels[k] = v
|
||||
}
|
||||
}
|
||||
if req.Proxy != nil {
|
||||
f.meta.Req.Proxy = req.Proxy
|
||||
}
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
if opts.Name != "" {
|
||||
f.meta.Opts.Name = opts.Name
|
||||
}
|
||||
if opts.Path != "" {
|
||||
f.meta.Opts.Path = opts.Path
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Pause() error {
|
||||
handle := f.currentHandle()
|
||||
if !handle.IsValid() {
|
||||
return nil
|
||||
}
|
||||
client, err := f.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.PauseTransfer(handle.GetHash()); err != nil {
|
||||
return err
|
||||
}
|
||||
f.handle = handle
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Close() error {
|
||||
if f.waitCancel != nil {
|
||||
f.waitCancel()
|
||||
}
|
||||
handle := f.currentHandle()
|
||||
if !handle.IsValid() {
|
||||
return nil
|
||||
}
|
||||
client, err := f.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.handle = handle
|
||||
return client.RemoveTransfer(handle.GetHash(), false)
|
||||
}
|
||||
|
||||
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||
return f.meta
|
||||
}
|
||||
|
||||
func (f *Fetcher) Stats() any {
|
||||
handle := f.currentHandle()
|
||||
if !handle.IsValid() {
|
||||
return &ped2k.Stats{}
|
||||
}
|
||||
status := handle.GetStatus()
|
||||
return &ped2k.Stats{
|
||||
State: string(status.State),
|
||||
ActivePeers: handle.ActiveConnections(),
|
||||
TotalPeers: status.NumPeers,
|
||||
DownloadRate: status.DownloadRate,
|
||||
Upload: status.Upload,
|
||||
UploadRate: status.UploadRate,
|
||||
TotalDone: status.TotalDone,
|
||||
TotalReceived: status.TotalReceived,
|
||||
TotalWanted: status.TotalWanted,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) Progress() fetcher.Progress {
|
||||
handle := f.currentHandle()
|
||||
if !handle.IsValid() {
|
||||
return fetcher.Progress{0}
|
||||
}
|
||||
return fetcher.Progress{handle.GetStatus().TotalReceived}
|
||||
}
|
||||
|
||||
func (f *Fetcher) Wait() error {
|
||||
client, err := f.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := f.hash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handle := f.currentHandle()
|
||||
if handle.IsValid() && handle.IsFinished() {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressCh, cancel := client.SubscribeTransferProgress()
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-f.waitCtx.Done():
|
||||
return nil
|
||||
case event, ok := <-progressCh:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, transfer := range event.Transfers {
|
||||
if transfer.Hash.Compare(hash) != 0 {
|
||||
continue
|
||||
}
|
||||
// Removal can happen during task deletion or client shutdown, both of
|
||||
// which should unblock Wait without treating it as a download failure.
|
||||
if transfer.Removed || transfer.State == goed2k.Finished {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) getClient() (*goed2k.Client, error) {
|
||||
if f.manager == nil {
|
||||
f.manager = &FetcherManager{}
|
||||
}
|
||||
return f.manager.initClient(f.config)
|
||||
}
|
||||
|
||||
func (f *Fetcher) currentHandle() goed2k.TransferHandle {
|
||||
if f.handle.IsValid() {
|
||||
return f.handle
|
||||
}
|
||||
if f.manager == nil {
|
||||
return f.handle
|
||||
}
|
||||
client := f.manager.currentClient()
|
||||
if client == nil {
|
||||
return f.handle
|
||||
}
|
||||
hash, err := f.hash()
|
||||
if err != nil {
|
||||
return f.handle
|
||||
}
|
||||
handle := client.FindTransfer(hash)
|
||||
if handle.IsValid() {
|
||||
f.handle = handle
|
||||
}
|
||||
return f.handle
|
||||
}
|
||||
|
||||
func (f *Fetcher) hash() (gprotocol.Hash, error) {
|
||||
if f.meta == nil || f.meta.Req == nil {
|
||||
return gprotocol.Invalid, errors.New("ed2k link is empty")
|
||||
}
|
||||
link, err := parseLink(f.meta.Req.URL)
|
||||
if err != nil {
|
||||
return gprotocol.Invalid, err
|
||||
}
|
||||
return link.Hash, nil
|
||||
}
|
||||
|
||||
type FetcherManager struct {
|
||||
mu sync.Mutex
|
||||
client *goed2k.Client
|
||||
stateStore *clientStateStore
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) SetStateStore(store fetcher.ProtocolStateStore) {
|
||||
fm.mu.Lock()
|
||||
defer fm.mu.Unlock()
|
||||
|
||||
if fm.stateStore == nil {
|
||||
fm.stateStore = &clientStateStore{}
|
||||
}
|
||||
fm.stateStore.store = store
|
||||
if fm.client != nil {
|
||||
fm.client.SetStateStore(fm.stateStore)
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Name() string {
|
||||
return "ed2k"
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||
return []*fetcher.SchemeFilter{
|
||||
{
|
||||
Type: fetcher.FilterTypeUrl,
|
||||
Pattern: "ED2K",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||
return &Fetcher{manager: fm}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) ParseName(u string) string {
|
||||
link, err := parseLink(u)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return link.StringValue
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) AutoRename() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) DefaultConfig() any {
|
||||
return &config{
|
||||
ListenPort: 0,
|
||||
UDPPort: 0,
|
||||
ServerAddr: defaultServerList,
|
||||
ServerMet: defaultServerMet,
|
||||
NodesDat: defaultNodesDat,
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Store(f fetcher.Fetcher) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||
return &Fetcher{
|
||||
manager: fm,
|
||||
meta: meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Close() error {
|
||||
fm.mu.Lock()
|
||||
defer fm.mu.Unlock()
|
||||
|
||||
if fm.client != nil {
|
||||
fm.client.Close()
|
||||
fm.client = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseLink(raw string) (goed2k.EMuleLink, error) {
|
||||
link, err := goed2k.ParseEMuleLink(raw)
|
||||
if err != nil {
|
||||
return goed2k.EMuleLink{}, err
|
||||
}
|
||||
if link.Type != goed2k.LinkFile {
|
||||
return goed2k.EMuleLink{}, errors.New("unsupported ed2k link type")
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func buildResource(link goed2k.EMuleLink) *base.Resource {
|
||||
return &base.Resource{
|
||||
Size: link.NumberValue,
|
||||
Range: false,
|
||||
Hash: link.Hash.String(),
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: link.StringValue,
|
||||
Size: link.NumberValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func splitCommaList(value string) []string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) getStateStoreLocked() *clientStateStore {
|
||||
if fm.stateStore == nil {
|
||||
fm.stateStore = &clientStateStore{}
|
||||
}
|
||||
return fm.stateStore
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) currentClient() *goed2k.Client {
|
||||
fm.mu.Lock()
|
||||
defer fm.mu.Unlock()
|
||||
return fm.client
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) initClient(cfg *config) (*goed2k.Client, error) {
|
||||
fm.mu.Lock()
|
||||
defer fm.mu.Unlock()
|
||||
|
||||
if fm.client != nil {
|
||||
return fm.client, nil
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = fm.DefaultConfig().(*config)
|
||||
}
|
||||
|
||||
settings := goed2k.NewSettings()
|
||||
settings.ListenPort = cfg.ListenPort
|
||||
settings.UDPPort = cfg.UDPPort
|
||||
settings.EnableDHT = true
|
||||
settings.EnableUPnP = true
|
||||
settings.ReconnectToServer = true
|
||||
|
||||
client := goed2k.NewClient(settings)
|
||||
client.SetStateStore(fm.getStateStoreLocked())
|
||||
if err := client.LoadState(""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fm.client = client
|
||||
// Bootstrap is best-effort: downloads can still proceed later even if
|
||||
// server list or DHT initialization fails during startup.
|
||||
bootstrapClient(client, cfg)
|
||||
return fm.client, nil
|
||||
}
|
||||
|
||||
func bootstrapClient(client *goed2k.Client, cfg *config) {
|
||||
for _, serverAddr := range splitCommaList(cfg.ServerAddr) {
|
||||
go func(serverAddr string) {
|
||||
_ = client.Connect(serverAddr)
|
||||
}(serverAddr)
|
||||
}
|
||||
for _, source := range splitCommaList(cfg.ServerMet) {
|
||||
go func(source string) {
|
||||
_ = client.ConnectServerMet(source)
|
||||
}(source)
|
||||
}
|
||||
if cfg.NodesDat != "" {
|
||||
go func() {
|
||||
_ = client.LoadDHTNodesDat(cfg.NodesDat)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package ed2k
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
const testLink = "ed2k://|file|cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso|4630972416|8867C5E54405FF9452225B66EFEE690A|/"
|
||||
|
||||
func TestFetcher_Resolve(t *testing.T) {
|
||||
f := (&FetcherManager{}).Build()
|
||||
f.Setup(controller.NewController())
|
||||
|
||||
err := f.Resolve(&base.Request{URL: testLink}, &base.Options{Path: t.TempDir()})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
|
||||
meta := f.Meta()
|
||||
if meta.Res == nil {
|
||||
t.Fatal("Resolve() resource is nil")
|
||||
}
|
||||
if got, want := meta.Res.Hash, "8867C5E54405FF9452225B66EFEE690A"; got != want {
|
||||
t.Fatalf("Resolve() hash = %s, want %s", got, want)
|
||||
}
|
||||
if got, want := meta.Res.Size, int64(4630972416); got != want {
|
||||
t.Fatalf("Resolve() size = %d, want %d", got, want)
|
||||
}
|
||||
if got, want := meta.Res.Files[0].Name, "cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso"; got != want {
|
||||
t.Fatalf("Resolve() name = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetcherManager_ParseName(t *testing.T) {
|
||||
got := (&FetcherManager{}).ParseName(testLink)
|
||||
want := "cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso"
|
||||
if got != want {
|
||||
t.Fatalf("ParseName() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -618,6 +618,7 @@ func (f *Fetcher) doStart() error {
|
||||
for _, conn := range f.connections {
|
||||
// Reset connections that can be retried
|
||||
if !conn.Completed && conn.State != connCompleted {
|
||||
f.resetConnectionForRestart(conn)
|
||||
conn.State = connNotStarted
|
||||
conn.failed = false
|
||||
conn.retryTimes = 0
|
||||
@@ -1413,6 +1414,27 @@ func (f *Fetcher) helpOtherConnection(helper *connection) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *Fetcher) resetConnectionForRestart(conn *connection) {
|
||||
if f.meta.Res.Range {
|
||||
return
|
||||
}
|
||||
|
||||
// Without range support a new request always starts from byte 0,
|
||||
// so pause/retry must restart instead of continuing from the old offset.
|
||||
if conn.Chunk == nil {
|
||||
conn.Chunk = newChunk(0, 0)
|
||||
} else {
|
||||
conn.Chunk.Begin = 0
|
||||
conn.Chunk.End = 0
|
||||
conn.Chunk.Downloaded = 0
|
||||
}
|
||||
conn.Downloaded = 0
|
||||
conn.Completed = false
|
||||
conn.speed = 0
|
||||
conn.lastSpeedCheck = 0
|
||||
conn.lastSpeedDownload = 0
|
||||
}
|
||||
|
||||
func (f *Fetcher) resumeConnections() {
|
||||
// Collect connections to resume while holding the lock
|
||||
var toResume []*connection
|
||||
@@ -1436,6 +1458,7 @@ func (f *Fetcher) resumeConnections() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
f.resetConnectionForRestart(conn)
|
||||
// Reset the connection state for resume
|
||||
conn.ctx, conn.cancel = context.WithCancel(f.ctx)
|
||||
conn.State = connNotStarted
|
||||
@@ -1475,7 +1498,16 @@ func (f *Fetcher) onDownloadComplete() {
|
||||
// Check if all chunks are complete (no remaining bytes)
|
||||
allChunksComplete := true
|
||||
for _, conn := range f.connections {
|
||||
if conn.Chunk != nil && conn.Chunk.remain() > 0 && !conn.Completed && conn.State != connCompleted {
|
||||
needsMoreData := false
|
||||
if f.meta.Res.Range {
|
||||
needsMoreData = conn.Chunk != nil && conn.Chunk.remain() > 0
|
||||
} else if f.meta.Res.Size > 0 {
|
||||
needsMoreData = conn.Downloaded < f.meta.Res.Size
|
||||
} else {
|
||||
needsMoreData = !conn.Completed && conn.State != connCompleted
|
||||
}
|
||||
|
||||
if needsMoreData && !conn.Completed && conn.State != connCompleted {
|
||||
// This connection has remaining work and isn't done
|
||||
// Check if it failed with 403 (server limit) - these can be ignored if other connections completed the work
|
||||
if conn.State == connFailed && conn.failed {
|
||||
|
||||
@@ -299,6 +299,53 @@ func TestFetcher_DownloadContinue(t *testing.T) {
|
||||
downloadContinue(listener, 16, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadContinue_NoRangeRestart(t *testing.T) {
|
||||
listener := test.StartTestNoRangeSlowServer(time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
fetcher := downloadReady(listener, 4, t)
|
||||
if err := fetcher.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
stats := fetcher.Stats().(*http.Stats)
|
||||
if len(stats.Connections) != 1 {
|
||||
t.Fatalf("expected a single non-range connection, got %d", len(stats.Connections))
|
||||
}
|
||||
if stats.Connections[0].Downloaded <= 0 || stats.Connections[0].Downloaded >= test.BuildSize {
|
||||
t.Fatalf("expected partial download before pause, got %d", stats.Connections[0].Downloaded)
|
||||
}
|
||||
|
||||
if err := fetcher.Pause(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := fetcher.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := fetcher.Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
finalStats := fetcher.Stats().(*http.Stats)
|
||||
if len(finalStats.Connections) != 1 {
|
||||
t.Fatalf("expected a single non-range connection after resume, got %d", len(finalStats.Connections))
|
||||
}
|
||||
if finalStats.Connections[0].Downloaded != test.BuildSize {
|
||||
t.Fatalf("downloaded bytes should restart cleanly: got %d, want %d", finalStats.Connections[0].Downloaded, test.BuildSize)
|
||||
}
|
||||
if total := fetcher.Progress().TotalDownloaded(); total != test.BuildSize {
|
||||
t.Fatalf("progress total = %d, want %d", total, test.BuildSize)
|
||||
}
|
||||
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("Download() got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadChunked(t *testing.T) {
|
||||
listener := test.StartTestCustomServer()
|
||||
defer listener.Close()
|
||||
@@ -1384,8 +1431,8 @@ func TestFetcher_Patch_Extra(t *testing.T) {
|
||||
Method: "POST", // Update method
|
||||
// Body is empty, should NOT update
|
||||
Header: map[string]string{
|
||||
"X-Custom": "modified", // Overwrite existing
|
||||
"X-New": "added", // Add new
|
||||
"X-Custom": "modified", // Overwrite existing
|
||||
"X-New": "added", // Add new
|
||||
// Authorization is not in patch, should remain
|
||||
},
|
||||
},
|
||||
|
||||
@@ -335,6 +335,43 @@ func StartTestOneTimeServer() net.Listener {
|
||||
})
|
||||
}
|
||||
|
||||
// StartTestNoRangeSlowServer creates a server that always returns the full file
|
||||
// with Content-Length but does not support Range requests.
|
||||
func StartTestNoRangeSlowServer(delayPerChunk time.Duration) net.Listener {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||
|
||||
file, err := os.Open(BuildFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := make([]byte, 256*1024)
|
||||
for !sl.isShutdown {
|
||||
n, readErr := file.Read(buf)
|
||||
if n > 0 {
|
||||
if _, writeErr := writer.Write(buf[:n]); writeErr != nil {
|
||||
return
|
||||
}
|
||||
if flusher, ok := writer.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
if delayPerChunk > 0 {
|
||||
time.Sleep(delayPerChunk)
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return mux
|
||||
})
|
||||
}
|
||||
|
||||
// StartTestExpiringRedirectServer creates a server that simulates expiring redirect URLs.
|
||||
// The original URL redirects to a temporary URL that expires after a specified number of requests.
|
||||
// When the temporary URL expires (returns 403), the client should retry with the original URL
|
||||
|
||||
+51
-11
@@ -31,6 +31,8 @@ const (
|
||||
bucketTask = "task"
|
||||
// task download data bucket
|
||||
bucketSave = "save"
|
||||
// protocol-level shared client state bucket
|
||||
bucketProtocolState = "protocol_state"
|
||||
// downloader config bucket
|
||||
bucketConfig = "config"
|
||||
// downloader extension bucket
|
||||
@@ -141,7 +143,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
||||
|
||||
func (d *Downloader) Setup() error {
|
||||
// setup storage
|
||||
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension, bucketExtensionStorage}); err != nil {
|
||||
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketProtocolState, bucketConfig, bucketExtension, bucketExtensionStorage}); err != nil {
|
||||
return err
|
||||
}
|
||||
// load config from storage
|
||||
@@ -165,6 +167,12 @@ func (d *Downloader) Setup() error {
|
||||
if _, ok := d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol]; !ok {
|
||||
d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol] = fm.DefaultConfig()
|
||||
}
|
||||
if sfm, ok := fm.(fetcher.StatefulFetcherManager); ok {
|
||||
sfm.SetStateStore(&protocolStateStore{
|
||||
storage: d.storage,
|
||||
protocol: protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// load tasks from storage
|
||||
@@ -358,9 +366,16 @@ func (d *Downloader) saveTask(task *Task) error {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
||||
return err
|
||||
}
|
||||
if err := d.storage.Put(bucketSave, task.ID, data); err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID)
|
||||
return err
|
||||
if data != nil {
|
||||
if err := d.storage.Put(bucketSave, task.ID, data); err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := d.storage.Delete(bucketSave, task.ID); err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("clear fetcher state failed: %s", task.ID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := d.storage.Put(bucketTask, task.ID, task); err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID)
|
||||
@@ -820,6 +835,26 @@ func (d *Downloader) Clear() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type protocolStateStore struct {
|
||||
storage Storage
|
||||
protocol string
|
||||
}
|
||||
|
||||
func (s *protocolStateStore) Load(v any) (bool, error) {
|
||||
return s.storage.Get(bucketProtocolState, s.protocol, v)
|
||||
}
|
||||
|
||||
func (s *protocolStateStore) Save(v any) error {
|
||||
if v == nil {
|
||||
return s.Delete()
|
||||
}
|
||||
return s.storage.Put(bucketProtocolState, s.protocol, v)
|
||||
}
|
||||
|
||||
func (s *protocolStateStore) Delete() error {
|
||||
return s.storage.Delete(bucketProtocolState, s.protocol)
|
||||
}
|
||||
|
||||
func (d *Downloader) Listener(fn Listener) {
|
||||
d.listener = fn
|
||||
}
|
||||
@@ -1186,7 +1221,7 @@ func (d *Downloader) statusMut(task *Task, fn func() (bool, error)) (bool, error
|
||||
}
|
||||
|
||||
func (d *Downloader) doStart(task *Task) (err error) {
|
||||
var isCreate bool
|
||||
var needCreate bool
|
||||
isReturn, err := d.statusMut(task, func() (isReturn bool, err error) {
|
||||
if task.Status == base.DownloadStatusRunning || task.Status == base.DownloadStatusDone {
|
||||
isReturn = true
|
||||
@@ -1198,7 +1233,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
||||
return
|
||||
}
|
||||
isCreate = task.Status == base.DownloadStatusReady
|
||||
needCreate = !task.IsCreated
|
||||
task.updateStatus(base.DownloadStatusRunning)
|
||||
|
||||
return
|
||||
@@ -1224,7 +1259,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
task.Meta.Res = task.fetcher.Meta().Res
|
||||
}
|
||||
|
||||
if isCreate {
|
||||
if needCreate {
|
||||
if task.fetcherManager.AutoRename() {
|
||||
d.checkDuplicateLock.Lock()
|
||||
defer d.checkDuplicateLock.Unlock()
|
||||
@@ -1248,10 +1283,9 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
task.Meta.Opts.Name = newName
|
||||
}
|
||||
}
|
||||
|
||||
task.IsCreated = true
|
||||
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
|
||||
}
|
||||
|
||||
task.Progress.Speed = 0
|
||||
task.timer.Start()
|
||||
if err := task.fetcher.Start(); err != nil {
|
||||
@@ -1300,8 +1334,14 @@ func (d *Downloader) doPause(task *Task) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
return err
|
||||
if task.fetcherManager != nil && task.fetcher != nil {
|
||||
if err := d.saveTask(task); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
d.emit(EventKeyPause, task)
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/ed2k"
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
@@ -26,6 +27,7 @@ type Task struct {
|
||||
Status base.Status `json:"status"`
|
||||
Uploading bool `json:"uploading"`
|
||||
Progress *Progress `json:"progress"`
|
||||
IsCreated bool `json:"isCreated"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
@@ -48,6 +50,7 @@ func NewTask() *Task {
|
||||
Status: base.DownloadStatusReady,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
IsCreated: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +113,14 @@ func (t *Task) updateUploadSpeed(downloaded int64, usedTime float64) int64 {
|
||||
}
|
||||
|
||||
func calcSpeed(speedArr *[]int64, downloaded int64, usedTime float64) int64 {
|
||||
if usedTime <= 0 {
|
||||
return 0
|
||||
}
|
||||
if downloaded < 0 {
|
||||
*speedArr = (*speedArr)[:0]
|
||||
return 0
|
||||
}
|
||||
|
||||
*speedArr = append(*speedArr, downloaded)
|
||||
// Record last 5 seconds of download speed to calculate the average speed
|
||||
if len(*speedArr) > int(5.0/usedTime) {
|
||||
@@ -156,6 +167,7 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
||||
cfg.FetchManagers = []fetcher.FetcherManager{
|
||||
new(http.FetcherManager),
|
||||
new(bt.FetcherManager),
|
||||
new(ed2k.FetcherManager),
|
||||
}
|
||||
}
|
||||
if cfg.RefreshInterval == 0 {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package download
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCalcSpeedResetOnRollback(t *testing.T) {
|
||||
speedArr := []int64{1024, 2048, 4096}
|
||||
|
||||
if got := calcSpeed(&speedArr, -512, 1); got != 0 {
|
||||
t.Fatalf("calcSpeed() = %d, want 0 after rollback", got)
|
||||
}
|
||||
if len(speedArr) != 0 {
|
||||
t.Fatalf("speed window len = %d, want 0 after rollback", len(speedArr))
|
||||
}
|
||||
|
||||
if got := calcSpeed(&speedArr, 1024, 1); got != 1024 {
|
||||
t.Fatalf("calcSpeed() = %d, want 1024 after reset", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ed2k
|
||||
|
||||
type Stats struct {
|
||||
State string `json:"state"`
|
||||
Paused bool `json:"paused"`
|
||||
ActivePeers int `json:"activePeers"`
|
||||
TotalPeers int `json:"totalPeers"`
|
||||
DownloadRate int `json:"downloadRate"`
|
||||
Upload int64 `json:"upload"`
|
||||
UploadRate int `json:"uploadRate"`
|
||||
TotalDone int64 `json:"totalDone"`
|
||||
TotalReceived int64 `json:"totalReceived"`
|
||||
TotalWanted int64 `json:"totalWanted"`
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class DownloaderConfig {
|
||||
class ProtocolConfig {
|
||||
HttpConfig http = HttpConfig();
|
||||
BtConfig bt = BtConfig();
|
||||
Ed2kConfig ed2k = Ed2kConfig();
|
||||
|
||||
ProtocolConfig();
|
||||
|
||||
@@ -80,6 +81,28 @@ class BtConfig {
|
||||
Map<String, dynamic> toJson() => _$BtConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Ed2kConfig {
|
||||
int listenPort;
|
||||
int udpPort;
|
||||
String serverAddr;
|
||||
String serverMet;
|
||||
String nodesDat;
|
||||
|
||||
Ed2kConfig({
|
||||
this.listenPort = 0,
|
||||
this.udpPort = 0,
|
||||
this.serverAddr = '',
|
||||
this.serverMet = '',
|
||||
this.nodesDat = '',
|
||||
});
|
||||
|
||||
factory Ed2kConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$Ed2kConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$Ed2kConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ExtraConfig {
|
||||
String themeMode;
|
||||
|
||||
@@ -42,12 +42,15 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
|
||||
ProtocolConfig _$ProtocolConfigFromJson(Map<String, dynamic> json) =>
|
||||
ProtocolConfig()
|
||||
..http = HttpConfig.fromJson(json['http'] as Map<String, dynamic>)
|
||||
..bt = BtConfig.fromJson(json['bt'] as Map<String, dynamic>);
|
||||
..bt = BtConfig.fromJson(json['bt'] as Map<String, dynamic>)
|
||||
..ed2k = Ed2kConfig.fromJson(
|
||||
json['ed2k'] as Map<String, dynamic>? ?? <String, dynamic>{});
|
||||
|
||||
Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'http': instance.http.toJson(),
|
||||
'bt': instance.bt.toJson(),
|
||||
'ed2k': instance.ed2k.toJson(),
|
||||
};
|
||||
|
||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||
@@ -82,6 +85,23 @@ Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
||||
'seedTime': instance.seedTime,
|
||||
};
|
||||
|
||||
Ed2kConfig _$Ed2kConfigFromJson(Map<String, dynamic> json) => Ed2kConfig(
|
||||
listenPort: (json['listenPort'] as num?)?.toInt() ?? 0,
|
||||
udpPort: (json['udpPort'] as num?)?.toInt() ?? 0,
|
||||
serverAddr: json['serverAddr'] as String? ?? '',
|
||||
serverMet: json['serverMet'] as String? ?? '',
|
||||
nodesDat: json['nodesDat'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$Ed2kConfigToJson(Ed2kConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'listenPort': instance.listenPort,
|
||||
'udpPort': instance.udpPort,
|
||||
'serverAddr': instance.serverAddr,
|
||||
'serverMet': instance.serverMet,
|
||||
'nodesDat': instance.nodesDat,
|
||||
};
|
||||
|
||||
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
themeMode: json['themeMode'] as String? ?? '',
|
||||
locale: json['locale'] as String? ?? '',
|
||||
|
||||
@@ -6,7 +6,7 @@ part 'task.g.dart';
|
||||
|
||||
enum Status { ready, running, pause, wait, error, done }
|
||||
|
||||
enum Protocol { http, bt }
|
||||
enum Protocol { http, bt, ed2k }
|
||||
|
||||
// ExtractStatus enum matching Go backend
|
||||
enum ExtractStatus {
|
||||
|
||||
@@ -51,6 +51,7 @@ const _$StatusEnumMap = {
|
||||
const _$ProtocolEnumMap = {
|
||||
Protocol.http: 'http',
|
||||
Protocol.bt: 'bt',
|
||||
Protocol.ed2k: 'ed2k',
|
||||
};
|
||||
|
||||
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
||||
|
||||
@@ -297,7 +297,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
MenuItem(
|
||||
label: 'donate'.tr,
|
||||
onClick: (menuItem) => {
|
||||
launchUrl(Uri.parse("https://docs.gopeed.com/donate.html"),
|
||||
launchUrl(Uri.parse("https://gopeed.com/docs/donate"),
|
||||
mode: LaunchMode.externalApplication)
|
||||
},
|
||||
),
|
||||
|
||||
@@ -59,7 +59,7 @@ class CreateView extends GetView<CreateController> {
|
||||
final _btTrackerController = TextEditingController();
|
||||
final _archivePasswordController = TextEditingController();
|
||||
|
||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||
final _availableSchemes = ["http:", "https:", "magnet:", "ed2k:"];
|
||||
|
||||
final _skipVerifyCertController = false.obs;
|
||||
final _autoTorrentController = Rxn<bool>();
|
||||
@@ -116,6 +116,7 @@ class CreateView extends GetView<CreateController> {
|
||||
jsonDecode(jsonEncode(routerParams.req!.extra)));
|
||||
_btTrackerController.text = reqExtra.trackers.join("\n");
|
||||
},
|
||||
Protocol.ed2k: null,
|
||||
};
|
||||
if (routerParams.req?.extra != null) {
|
||||
extraHandlers[protocol]?.call();
|
||||
@@ -139,6 +140,7 @@ class CreateView extends GetView<CreateController> {
|
||||
}
|
||||
},
|
||||
Protocol.bt: null,
|
||||
Protocol.ed2k: null,
|
||||
};
|
||||
if (routerParams.opts?.extra != null) {
|
||||
optionsHandlers[protocol]?.call();
|
||||
@@ -804,6 +806,9 @@ class CreateView extends GetView<CreateController> {
|
||||
uppercaseUrl.endsWith(".TORRENT")) {
|
||||
protocol = Protocol.bt;
|
||||
}
|
||||
if (uppercaseUrl.startsWith("ED2K:")) {
|
||||
protocol = Protocol.ed2k;
|
||||
}
|
||||
return protocol;
|
||||
}
|
||||
|
||||
@@ -1007,6 +1012,9 @@ class CreateView extends GetView<CreateController> {
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
break;
|
||||
case Protocol.ed2k:
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
return reqExtra;
|
||||
}
|
||||
|
||||
@@ -501,14 +501,16 @@ class SettingView extends GetView<SettingController> {
|
||||
final buildDesktopNotification = !Util.isDesktop()
|
||||
? () => null
|
||||
: _buildConfigItem('desktopNotification', () {
|
||||
return appController.downloaderConfig.value.extra.desktopNotification
|
||||
return appController
|
||||
.downloaderConfig.value.extra.desktopNotification
|
||||
? 'on'.tr
|
||||
: 'off'.tr;
|
||||
}, (Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value: appController.downloaderConfig.value.extra.desktopNotification,
|
||||
value: appController
|
||||
.downloaderConfig.value.extra.desktopNotification,
|
||||
onChanged: (bool value) async {
|
||||
appController.downloaderConfig.update((val) {
|
||||
val!.extra.desktopNotification = value;
|
||||
@@ -787,6 +789,138 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
});
|
||||
|
||||
// ed2k config items start
|
||||
final ed2kConfig = downloaderCfg.value.protocolConfig.ed2k;
|
||||
List<String> parseEd2kEntries(String value) {
|
||||
return value
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
String summarizeEd2kEntries(String value) {
|
||||
final entries = parseEd2kEntries(value);
|
||||
if (entries.isEmpty) {
|
||||
return 'notSet'.tr;
|
||||
}
|
||||
return 'items'.trParams({'count': entries.length.toString()});
|
||||
}
|
||||
|
||||
String formatEd2kMultilineValue(String value) {
|
||||
return parseEd2kEntries(value).join('\r\n');
|
||||
}
|
||||
|
||||
final buildEd2kListenPort = _buildConfigItem(
|
||||
'ed2kTcpPort', () => ed2kConfig.listenPort.toString(), (Key key) {
|
||||
final controller =
|
||||
TextEditingController(text: ed2kConfig.listenPort.toString());
|
||||
controller.addListener(() async {
|
||||
if (controller.text.isNotEmpty &&
|
||||
controller.text != ed2kConfig.listenPort.toString()) {
|
||||
ed2kConfig.listenPort = int.parse(controller.text);
|
||||
await debounceSave();
|
||||
}
|
||||
});
|
||||
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 0, max: 65535),
|
||||
],
|
||||
);
|
||||
});
|
||||
final buildEd2kUdpPort = _buildConfigItem(
|
||||
'ed2kUdpPort', () => ed2kConfig.udpPort.toString(), (Key key) {
|
||||
final controller =
|
||||
TextEditingController(text: ed2kConfig.udpPort.toString());
|
||||
controller.addListener(() async {
|
||||
if (controller.text.isNotEmpty &&
|
||||
controller.text != ed2kConfig.udpPort.toString()) {
|
||||
ed2kConfig.udpPort = int.parse(controller.text);
|
||||
await debounceSave();
|
||||
}
|
||||
});
|
||||
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 0, max: 65535),
|
||||
],
|
||||
);
|
||||
});
|
||||
final buildEd2kServerAddr = _buildConfigItem(
|
||||
'ed2kServerList', () => summarizeEd2kEntries(ed2kConfig.serverAddr),
|
||||
(Key key) {
|
||||
final controller = TextEditingController(
|
||||
text: formatEd2kMultilineValue(ed2kConfig.serverAddr));
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'ed2kServersHint'.tr,
|
||||
helperText: 'ed2kOnePerLine'.tr,
|
||||
),
|
||||
onChanged: (value) async {
|
||||
ed2kConfig.serverAddr = Util.textToLines(value).join(',');
|
||||
await debounceSave();
|
||||
},
|
||||
);
|
||||
});
|
||||
final buildEd2kServerMet = _buildConfigItem(
|
||||
'ed2kServerMet', () => summarizeEd2kEntries(ed2kConfig.serverMet),
|
||||
(Key key) {
|
||||
final controller = TextEditingController(
|
||||
text: formatEd2kMultilineValue(ed2kConfig.serverMet));
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'ed2kServerMetHint'.tr,
|
||||
helperText: 'ed2kOnePerLine'.tr,
|
||||
),
|
||||
onChanged: (value) async {
|
||||
ed2kConfig.serverMet = Util.textToLines(value).join(',');
|
||||
await debounceSave();
|
||||
},
|
||||
);
|
||||
});
|
||||
final buildEd2kNodesDat = _buildConfigItem(
|
||||
'ed2kNodesDat', () => summarizeEd2kEntries(ed2kConfig.nodesDat),
|
||||
(Key key) {
|
||||
final controller = TextEditingController(
|
||||
text: formatEd2kMultilineValue(ed2kConfig.nodesDat));
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'ed2kNodesDatHint'.tr,
|
||||
helperText: 'ed2kOnePerLine'.tr,
|
||||
),
|
||||
onChanged: (value) async {
|
||||
ed2kConfig.nodesDat = Util.textToLines(value).join(',');
|
||||
await debounceSave();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ui config items start
|
||||
final buildTheme = _buildConfigItem(
|
||||
'theme',
|
||||
@@ -1648,6 +1782,17 @@ class SettingView extends GetView<SettingController> {
|
||||
buildBtDefaultClientConfig(),
|
||||
]),
|
||||
)),
|
||||
Text('ed2k'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildEd2kListenPort(),
|
||||
buildEd2kUdpPort(),
|
||||
buildEd2kServerAddr(),
|
||||
buildEd2kServerMet(),
|
||||
buildEd2kNodesDat(),
|
||||
]),
|
||||
)),
|
||||
Text('ui'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
const caES = {
|
||||
'ca_ES': {
|
||||
'label': 'Català',
|
||||
'error': 'Error',
|
||||
'tip': 'Consell',
|
||||
'confirm': 'Confirma',
|
||||
'confirmDelete': 'Esteu segur que voleu suprimir-lo?',
|
||||
'cancel': 'Cancel·la',
|
||||
'on': 'Activat',
|
||||
'off': 'Desactivat',
|
||||
'selectAll': 'Selecciona-ho tot',
|
||||
'select': 'Selecciona',
|
||||
'task': 'Tasques',
|
||||
'downloading': 's\'està baixant',
|
||||
'downloaded': 's\'ha baixat',
|
||||
'setting': 'Configuració',
|
||||
'donate': 'Donació',
|
||||
'exit': 'Surt',
|
||||
'create': 'Crea una tasca',
|
||||
'directDownload': 'Baixada directa',
|
||||
'advancedOptions': 'Opcions avançades',
|
||||
'followSettings': 'Segueix la configuració',
|
||||
'downloadLink': 'Enllaç de baixada',
|
||||
'downloadLinkValid': 'Introduïu l\'enllaç de baixada',
|
||||
'downloadLinkHit': 'Introduïu l\'enllaç de baixada, un per línia@append',
|
||||
'downloadLinkHitDesktop': 'o arrossegueu el fitxer torrent aquí',
|
||||
'download': 'Baixa',
|
||||
'noFileSelected': 'Seleccioneu almenys un fitxer per continuar.',
|
||||
'noStoragePermission': 'Cal permís d\'emmagatzematge',
|
||||
'selectFile': 'Seleccioneu el fitxer',
|
||||
'rename': 'Canvia el nom',
|
||||
'basic': 'Bàsic',
|
||||
'advanced': 'Avançat',
|
||||
'general': 'General',
|
||||
'downloadDir': 'Carpeta de baixades',
|
||||
'downloadDirValid': 'Seleccioneu la carpeta de baixades',
|
||||
'connections': 'Connexions',
|
||||
'useServerCtime':
|
||||
'Utilitza l\'hora del servidor per a la creació de fitxers',
|
||||
'maxRunning': 'Màxim de tasques en execució',
|
||||
'defaultDirectDownload': 'Activa la baixada directa per defecte',
|
||||
'autoStartTasks':
|
||||
'Inicia automàticament les tasques incompletes a l\'inici',
|
||||
'autoTorrentEnable':
|
||||
'Crea automàticament tasques BT a partir de fitxers .torrent',
|
||||
'autoTorrentDeleteAfterDownload':
|
||||
'Suprimeix el fitxer .torrent després de crear la tasca BT',
|
||||
'autoDeleteMissingFileTasks':
|
||||
'Suprimeix automàticament les tasques dels fitxers que falten',
|
||||
'items': '@count elements',
|
||||
'subscribeTracker': 'Subscripció al rastrejador',
|
||||
'subscribeFail':
|
||||
'La subscripció ha fallat. Comproveu la xarxa o torneu-ho a provar més tard',
|
||||
'update': 'Actualitza',
|
||||
'updateDaily': 'Actualització diària',
|
||||
'lastUpdate': 'Última actualització: @time',
|
||||
'addTracker': 'Afegeix un rastrejador',
|
||||
'addTrackerHit':
|
||||
'Introduïu la URL del servidor del rastrejador, un per línia',
|
||||
'ui': 'UI',
|
||||
'theme': 'Tema',
|
||||
'themeSystem': 'Sistema',
|
||||
'themeLight': 'Clar',
|
||||
'themeDark': 'Fosc',
|
||||
'locale': 'Idioma',
|
||||
'notifyWhenNewVersion': 'Notificació d\'actualitzacions',
|
||||
'analyticsEnabled': 'Puja les estadístiques',
|
||||
'analyticsEnabledDesc':
|
||||
'Compartiu dades d\'ús anònimes per ajudar-nos a millorar',
|
||||
'about': 'Quant a',
|
||||
'homepage': 'Pàgina d\'inici',
|
||||
'version': 'Versió',
|
||||
'protocol': 'Protocol',
|
||||
'port': 'Port',
|
||||
'apiToken': 'Token de l\'API',
|
||||
'notSet': 'Sense congigurar',
|
||||
'set': 'Configurat',
|
||||
'portInUse': 'El port [@port] està en ús, canvieu-lo',
|
||||
'effectAfterRestart': 'Els efectes s\'aplicaran després de reiniciar',
|
||||
'developer': 'Desenvolupador',
|
||||
'logDirectory': 'Carpeta dels registres',
|
||||
'webhook': 'Webhook',
|
||||
'webhookEnable': 'Activa el Webhook',
|
||||
'webhookDesc':
|
||||
'Envia notificacions HTTP POST quan les tasques finalitzin o fallin',
|
||||
'webhookUrlHint': 'Introduïu la URL del webhook',
|
||||
'webhookTest': 'Prova',
|
||||
'webhookTestSuccess': 'La prova del webhook ha estat correcta',
|
||||
'webhookTestFail': 'La prova del webhook ha fallat',
|
||||
'script': 'Seqüència d\'ordres',
|
||||
'scriptEnable': 'Activa la seqüència d\'ordres',
|
||||
'scriptDesc':
|
||||
'Executa seqüències d\'ordres personalitzades quan les baixades finalitzin correctament',
|
||||
'scriptPathHint':
|
||||
'Introduïu la ruta del fitxer de seqüències d\'ordres (p.ex., /ruta/al/fitxer.sh)',
|
||||
'urlInvalid': 'Introduïu una URL HTTP o HTTPS vàlida',
|
||||
'required': 'Aquest camp és obligatori',
|
||||
'show': 'Mostra',
|
||||
'continue': 'Contina',
|
||||
'pause': 'Pausa',
|
||||
'startAll': 'Inicia-ho tot',
|
||||
'pauseAll': 'Pausa-ho tot',
|
||||
'deleteTask': 'Suprimeix @count tasques',
|
||||
'deleteTaskTip': 'Mantén els fitxers baixats',
|
||||
'delete': 'Suprimeix',
|
||||
'add': 'Afegeix',
|
||||
'edit': 'Edita',
|
||||
'newVersionTitle': 'Descobriu la nova versió @version',
|
||||
'newVersionUpdate': 'Actualitza ara',
|
||||
'newVersionLater': 'Més tard',
|
||||
'extensions': 'Extensions',
|
||||
'extensionInstallUrl': 'URL d\'instal·lació',
|
||||
'extensionInstallSuccess': 'S\'ha instal·lat amb èxit',
|
||||
'extensionUpdateSuccess': 'S\'ha actualitzat amb èxit',
|
||||
'extensionDelete': 'Suprimeix l\'extensió',
|
||||
'extensionAlreadyLatest': 'Ja està actualitzada',
|
||||
'extensionFind': 'Cerca extensions',
|
||||
'extensionDevelop': 'Desenvolupa extensions',
|
||||
'extensionStoreTitle': 'Botiga d\'extensions',
|
||||
'extensionStoreSection': 'Extensions de la botiga',
|
||||
'extensionInstalledSection': 'Extensions instal·lades',
|
||||
'extensionInstalledEmpty': 'Encara no s\'ha instal·lat cap extensió',
|
||||
'extensionStoreEmpty': 'No s\'han trobat extensions coincidents',
|
||||
'extensionSearchHint': 'Cerca per nom, títol, descripció o autor',
|
||||
'extensionSortStars': 'Més estrelles',
|
||||
'extensionSortInstalls': 'Més instal·lacions',
|
||||
'extensionSortUpdated': 'Actualitzada recentment',
|
||||
'extensionSortToggle': 'Commuta la direcció d\'ordenació',
|
||||
'extensionLoadMore': 'Carrega\'n més',
|
||||
'extensionNoMore': 'No hi ha més extensions',
|
||||
'extensionInstalled': 'Instal·lada',
|
||||
'extensionCanUpdate': 'Actualització disponible',
|
||||
'extensionInstall': 'Instal·la',
|
||||
'extensionLoadLocal': 'Instal·la de d\'una carpeta local',
|
||||
'extensionFilterAll': 'Totes',
|
||||
'extensionFilterMarket': 'Mercat',
|
||||
'extensionFilterInstalled': 'Instal·lada',
|
||||
'extensionManualInstall': 'Instal·lació manual',
|
||||
'extensionInstallTools': 'Opcions d\'instal·lació',
|
||||
'history': 'Historial',
|
||||
'clearHistory': 'Neteja l\'historial',
|
||||
'noHistoryFound': 'No s\'ha trobat cap historial',
|
||||
'serviceTitle': 'Servei de baixades',
|
||||
'serviceText': 'S\'està executant',
|
||||
'network': 'Xarxa',
|
||||
'proxy': 'Servidor intermediari',
|
||||
'noProxy': 'Sense servidor intermediari',
|
||||
'systemProxy': 'Servidor intermediari del sistema',
|
||||
'customProxy': 'Servidor intermediari personalitzat',
|
||||
'server': 'Servidor',
|
||||
'username': 'Nom d\'usuari',
|
||||
'password': 'Contrasenya',
|
||||
'thanks': 'Gràcies',
|
||||
'thanksDesc':
|
||||
'Gràcies a tots els col·laboradors que han ajudat a construir i desenvolupar la comunitat del Gopeed!',
|
||||
'browserExtension': 'Extensió del navegador',
|
||||
'launchAtStartup': 'Executa a l\'inici',
|
||||
'runAsMenubarApp': 'Executa com a aplicació de barra de menú',
|
||||
'runAsMenubarAppDesc':
|
||||
'Amaga la icona de l\'acoblador i executa només a la barra de menú',
|
||||
'seedConfig': 'Configuració de les llavors',
|
||||
'seedKeep': 'Continua sembrant fins que s\'aturi manualment',
|
||||
'seedRatio': 'Proporció de les llavors',
|
||||
'seedTime': 'Temps de la llavor (minuts)',
|
||||
'setAsDefaultBtClient': 'Estableix com a client BT per defecte',
|
||||
'taskDetail': 'Detall de la tasca',
|
||||
'taskName': 'Nom de la tasca',
|
||||
'taskUrl': 'URL de la tasca',
|
||||
'downloadPath': 'Ruta de la baixada',
|
||||
'skipVerifyCert': 'Omet la verificació del certificat',
|
||||
'archives': 'Comprimits',
|
||||
'autoExtract': 'Extreu els comprimits automàticament',
|
||||
'archivePassword': 'Contrasenya del comprimit',
|
||||
'archivePasswordHint':
|
||||
'Deixeu-lo en blanc si no està protegit amb contrasenya',
|
||||
'deleteAfterExtract': 'Suprimeix el comprimit després de l\'extracció',
|
||||
'extracting': 'S\'està extraient',
|
||||
'extractDone': 'Extracció completada',
|
||||
'extractError': 'Extracció fallida',
|
||||
'waitingParts': 'S\'està esperant a les parts',
|
||||
'name': 'Nom',
|
||||
'size': 'Mida',
|
||||
'unknown': 'Desconegut',
|
||||
'fileSelectedCount': 'Fitxers: ',
|
||||
'fileSelectedSize': 'Mida: ',
|
||||
'httpHeader': 'Capçalera',
|
||||
'httpHeaderName': 'Nomm de la capçalera',
|
||||
'httpHeaderValue': 'Valor de la capçalera',
|
||||
'login': 'Inici de sessió',
|
||||
'username_required': 'Introduïu el vostre nom d\'usuari',
|
||||
'password_required': 'Introduïu la vostra contrasenya',
|
||||
'login_success': 'Inici de sessió correcte',
|
||||
'login_failed':
|
||||
'No s\'ha pogut iniciar la sessió, comproveu el vostre nom d\'usuari i contrasenya',
|
||||
'login_failed_network':
|
||||
'No s\'ha pogut iniciar la sessió, comproveu la vostra connexió de xarxa',
|
||||
'insertPlaceholder': 'Inseriu un marcador de posició',
|
||||
'placeholderYear': 'Any actual',
|
||||
'placeholderMonth': 'Mes actual (01-12)',
|
||||
'placeholderDay': 'Dia actual (01-31)',
|
||||
'placeholderDate': 'Data completa (AAAA-MM-DD)',
|
||||
'example': 'p.ex. @value',
|
||||
'downloadCategories': 'Categories de baixades',
|
||||
'categoryMusic': 'Música',
|
||||
'categoryVideo': 'Vídeo',
|
||||
'categoryDocument': 'Document',
|
||||
'categoryProgram': 'Programa',
|
||||
'categoryName': 'Nom de la categoria',
|
||||
'categoryPath': 'Ruta de la categoria',
|
||||
'builtInCategory': 'La categoria integrada no es pot suprimir',
|
||||
'selectCategory': 'Seleccioneu una categoria',
|
||||
'githubMirror': 'Rèplica del GitHub',
|
||||
'githubMirrorEnable': 'Activa la rèplica del GitHub',
|
||||
'githubMirrorDesc':
|
||||
'Utilitzeu rèpliques per accelerar les baixades de contingut del GitHub',
|
||||
'githubMirrorType': 'Tipus de rèplica',
|
||||
'githubMirrorUrl': 'URL de la rèplica',
|
||||
'githubMirrorUrlHint': 'Introduïu la URL de la rèplica',
|
||||
'updateUrl': 'Actualitza la URL',
|
||||
'updateUrlManual': 'Actualització manual',
|
||||
'updateUrlListen': 'Escolta l\'actualització',
|
||||
'updateUrlListeningTip': 'S\'està esperant l\'actualització de l\'URL',
|
||||
'updateUrlCancelListen': 'Cancel·la l\'escolta',
|
||||
'updateUrlDialogHint': 'Introduïu una nova URL de baixada',
|
||||
'pendingUpdateFound': 'S\'ha trobat una tasca d\'actualització pendent',
|
||||
'pendingUpdateConfirm':
|
||||
'La tasca "@name" està esperant l\'actualització de la URL. Voleu actualitzar-la amb la URL nova?',
|
||||
'pendingUpdateYes': 'Actualitza la tasca',
|
||||
'pendingUpdateNo': 'Crea una tasca nova',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Notificacions d\'escriptori',
|
||||
'notificationTaskDone': 'Tasca completada',
|
||||
'notificationTaskError': 'Error de tasca',
|
||||
},
|
||||
};
|
||||
@@ -164,6 +164,17 @@ const deDE = {
|
||||
'categoryPath': 'Kategoriepfad',
|
||||
'builtInCategory': 'Integrierte Kategorie kann nicht gelöscht werden',
|
||||
'selectCategory': 'Kategorie auswählen',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Desktop-Benachrichtigungen',
|
||||
'notificationTaskDone': 'Aufgabe abgeschlossen',
|
||||
'notificationTaskError': 'Aufgabenfehler',
|
||||
|
||||
@@ -214,6 +214,17 @@ const enUS = {
|
||||
'Task "@name" is waiting for URL update. Do you want to update it with the new URL?',
|
||||
'pendingUpdateYes': 'Update Task',
|
||||
'pendingUpdateNo': 'Create New Task',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Desktop Notifications',
|
||||
'notificationTaskDone': 'Task completed',
|
||||
'notificationTaskError': 'Task error',
|
||||
|
||||
@@ -142,6 +142,17 @@ const esES = {
|
||||
'categoryPath': 'Ruta de categoría',
|
||||
'builtInCategory': 'La categoría incorporada no se puede eliminar',
|
||||
'selectCategory': 'Seleccionar categoría',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Notificaciones de escritorio',
|
||||
'notificationTaskDone': 'Tarea completada',
|
||||
'notificationTaskError': 'Error de tarea',
|
||||
|
||||
@@ -93,6 +93,17 @@ const faIR = {
|
||||
'launchAtStartup': 'راهاندازی در استارتآپ',
|
||||
'runAsMenubarApp': 'اجرا به عنوان برنامه نوار منو',
|
||||
'runAsMenubarAppDesc': 'پنهان کردن آیکون Dock و اجرا فقط در نوار منو',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'اعلانهای دسکتاپ',
|
||||
'notificationTaskDone': 'وظیفه تکمیل شد',
|
||||
'notificationTaskError': 'خطای وظیفه',
|
||||
|
||||
@@ -135,6 +135,17 @@ const frFR = {
|
||||
'categoryPath': 'Chemin de la catégorie',
|
||||
'builtInCategory': 'La catégorie intégrée ne peut pas être supprimée',
|
||||
'selectCategory': 'Sélectionner une catégorie',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Notifications de bureau',
|
||||
'notificationTaskDone': 'Tâche terminée',
|
||||
'notificationTaskError': 'Erreur de tâche',
|
||||
|
||||
@@ -156,6 +156,17 @@ const huHU = {
|
||||
'categoryPath': 'Kategória útvonala',
|
||||
'builtInCategory': 'A beépített kategória nem törölhető',
|
||||
'selectCategory': 'Kategória kiválasztása',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Asztali értesítések',
|
||||
'notificationTaskDone': 'Feladat befejezve',
|
||||
'notificationTaskError': 'Feladat hiba',
|
||||
|
||||
@@ -142,6 +142,17 @@ const idID = {
|
||||
'categoryPath': 'Path Kategori',
|
||||
'builtInCategory': 'Kategori bawaan tidak dapat dihapus',
|
||||
'selectCategory': 'Pilih Kategori',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Notifikasi Desktop',
|
||||
'notificationTaskDone': 'Tugas selesai',
|
||||
'notificationTaskError': 'Tugas error',
|
||||
|
||||
@@ -130,6 +130,17 @@ const itIT = {
|
||||
'categoryPath': 'Percorso categoria',
|
||||
'builtInCategory': 'La categoria predefinita non può essere eliminata',
|
||||
'selectCategory': 'Seleziona categoria',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Notifiche Desktop',
|
||||
'notificationTaskDone': 'Attività completata',
|
||||
'notificationTaskError': 'Errore attività',
|
||||
|
||||
@@ -95,6 +95,17 @@ const jaJP = {
|
||||
'launchAtStartup': '起動時に起動',
|
||||
'runAsMenubarApp': 'メニューバーアプリとして実行',
|
||||
'runAsMenubarAppDesc': 'Dockアイコンを非表示にして、メニューバーのみで実行',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'デスクトップ通知',
|
||||
'notificationTaskDone': 'タスク完了',
|
||||
'notificationTaskError': 'タスクエラー',
|
||||
|
||||
@@ -127,6 +127,17 @@ const plPL = {
|
||||
'categoryPath': 'Ścieżka kategorii',
|
||||
'builtInCategory': 'Wbudowanej kategorii nie można usunąć',
|
||||
'selectCategory': 'Wybierz kategorię',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Powiadomienia na pulpicie',
|
||||
'notificationTaskDone': 'Zadanie zakończone',
|
||||
'notificationTaskError': 'Błąd zadania',
|
||||
|
||||
@@ -153,6 +153,17 @@ const ptBR = {
|
||||
'categoryPath': 'Caminho da Categoria',
|
||||
'builtInCategory': 'Categoria integrada não pode ser excluída',
|
||||
'selectCategory': 'Selecionar Categoria',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Notificações na área de trabalho',
|
||||
'notificationTaskDone': 'Tarefa concluída',
|
||||
'notificationTaskError': 'Erro na tarefa',
|
||||
|
||||
@@ -152,6 +152,17 @@ const ruRU = {
|
||||
'categoryPath': 'Путь категории',
|
||||
'builtInCategory': 'Встроенную категорию нельзя удалить',
|
||||
'selectCategory': 'Выбрать категорию',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Уведомления',
|
||||
'notificationTaskDone': 'Задача завершена',
|
||||
'notificationTaskError': 'Ошибка задачи',
|
||||
|
||||
@@ -129,6 +129,17 @@ const taTA = {
|
||||
'categoryPath': 'வகை பாதை',
|
||||
'builtInCategory': 'உள்ளமைக்கப்பட்ட வகையை நீக்க முடியாது',
|
||||
'selectCategory': 'வகையைத் தேர்ந்தெடுக்கவும்',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'கணினி அறிவிப்புகள்',
|
||||
'notificationTaskDone': 'பணி முடிந்தது',
|
||||
'notificationTaskError': 'பணி பிழை',
|
||||
|
||||
@@ -22,8 +22,10 @@ const trTR = {
|
||||
'followSettings': 'Ayarları Takip Et',
|
||||
'downloadLink': 'İndirme Bağlantısı',
|
||||
'downloadLinkValid': 'Lütfen indirme bağlantısını girin',
|
||||
'downloadLinkHit': 'Lütfen her satıra bir tane indirme bağlantısını girin@append',
|
||||
'downloadLinkHitDesktop': ', veya torrent dosyasını olarak buraya sürükleyin',
|
||||
'downloadLinkHit':
|
||||
'Lütfen her satıra bir tane indirme bağlantısını girin@append',
|
||||
'downloadLinkHitDesktop':
|
||||
', veya torrent dosyasını olarak buraya sürükleyin',
|
||||
'download': 'İndir',
|
||||
'noFileSelected': 'Devam etmek için lütfen en az bir dosya seçin.',
|
||||
'noStoragePermission': 'Depolama izni gerekli',
|
||||
@@ -39,7 +41,8 @@ const trTR = {
|
||||
'maxRunning': 'Maksimum Çalışan İşlem',
|
||||
'defaultDirectDownload': 'Varsayılan olarak direkt indirmeyi işaretle',
|
||||
'autoStartTasks': 'Başlangıçta tamamlanmamış işlemleri otomatik başlat',
|
||||
'autoTorrentEnable': '.torrent dosyalarından otomatik BitTorrent işlemleri oluştur',
|
||||
'autoTorrentEnable':
|
||||
'.torrent dosyalarından otomatik BitTorrent işlemleri oluştur',
|
||||
'autoTorrentDeleteAfterDownload':
|
||||
'BitTorrent işlemi oluşturulduktan sonra .torrent dosyasını sil',
|
||||
'autoDeleteMissingFileTasks': 'Eksik dosya işlemlerini otomatik sil',
|
||||
@@ -51,7 +54,8 @@ const trTR = {
|
||||
'updateDaily': 'Günlük güncelle',
|
||||
'lastUpdate': 'Son güncelleme: @time',
|
||||
'addTracker': 'Tracker Ekle',
|
||||
'addTrackerHit': 'Lütfen her satıra bir tane olacak şekilde tracker sunucu adresini girin',
|
||||
'addTrackerHit':
|
||||
'Lütfen her satıra bir tane olacak şekilde tracker sunucu adresini girin',
|
||||
'ui': 'Arayüz',
|
||||
'theme': 'Tema',
|
||||
'themeSystem': 'Sistem',
|
||||
@@ -60,7 +64,8 @@ const trTR = {
|
||||
'locale': 'Dil',
|
||||
'notifyWhenNewVersion': 'Güncellemeleri bildir',
|
||||
'analyticsEnabled': 'İstatistikleri Yükle',
|
||||
'analyticsEnabledDesc': 'Uygulamayı geliştirmemize yardımcı olmak için anonim kullanım verilerini paylaşın',
|
||||
'analyticsEnabledDesc':
|
||||
'Uygulamayı geliştirmemize yardımcı olmak için anonim kullanım verilerini paylaşın',
|
||||
'about': 'Hakkında',
|
||||
'homepage': 'Ana Sayfa',
|
||||
'version': 'Sürüm',
|
||||
@@ -75,14 +80,16 @@ const trTR = {
|
||||
'logDirectory': 'Günlük Dizini',
|
||||
'webhook': 'Webhook',
|
||||
'webhookEnable': 'Webhook\'u Etkinleştir',
|
||||
'webhookDesc': 'İşlemler tamamlandığında veya başarısız olduğunda HTTP POST bildirimleri gönder',
|
||||
'webhookDesc':
|
||||
'İşlemler tamamlandığında veya başarısız olduğunda HTTP POST bildirimleri gönder',
|
||||
'webhookUrlHint': 'Webhook URL\'sini girin',
|
||||
'webhookTest': 'Test',
|
||||
'webhookTestSuccess': 'Webhook testi başarılı',
|
||||
'webhookTestFail': 'Webhook testi başarısız',
|
||||
'script': 'Betik',
|
||||
'scriptEnable': 'Betiği Etkinleştir',
|
||||
'scriptDesc': 'İndirmeler başarıyla tamamlandığında özel betikleri çalıştır',
|
||||
'scriptDesc':
|
||||
'İndirmeler başarıyla tamamlandığında özel betikleri çalıştır',
|
||||
'scriptPathHint': 'Betik dosya yolunu girin (örn. /path/to/script.sh)',
|
||||
'urlInvalid': 'Lütfen geçerli bir HTTP veya HTTPS URL\'si girin',
|
||||
'required': 'Bu alan zorunludur',
|
||||
@@ -107,6 +114,27 @@ const trTR = {
|
||||
'extensionAlreadyLatest': 'Zaten en son sürüm',
|
||||
'extensionFind': 'Eklenti Bul',
|
||||
'extensionDevelop': 'Eklenti Geliştir',
|
||||
'extensionStoreTitle': 'Eklenti Mağazası',
|
||||
'extensionStoreSection': 'Mağaza Eklentileri',
|
||||
'extensionInstalledSection': 'Yüklü Eklentiler',
|
||||
'extensionInstalledEmpty': 'Henüz yüklü eklenti yok',
|
||||
'extensionStoreEmpty': 'Eşleşen eklenti bulunamadı',
|
||||
'extensionSearchHint': 'Ada, başlığa, açıklamaya veya yazara göre ara',
|
||||
'extensionSortStars': 'En Çok Yıldız Alanlar',
|
||||
'extensionSortInstalls': 'En Çok Yüklenenler',
|
||||
'extensionSortUpdated': 'Son Güncellenenler',
|
||||
'extensionSortToggle': 'Sıralama yönünü değiştir',
|
||||
'extensionLoadMore': 'Daha Fazla Yükle',
|
||||
'extensionNoMore': 'Daha fazla eklenti yok',
|
||||
'extensionInstalled': 'Yüklü',
|
||||
'extensionCanUpdate': 'Güncelleme Mevcut',
|
||||
'extensionInstall': 'Yükle',
|
||||
'extensionLoadLocal': 'Yerel Klasörden Yükle',
|
||||
'extensionFilterAll': 'Tümü',
|
||||
'extensionFilterMarket': 'Market',
|
||||
'extensionFilterInstalled': 'Yüklü',
|
||||
'extensionManualInstall': 'Manuel Yükleme',
|
||||
'extensionInstallTools': 'Yükleme Seçenekleri',
|
||||
'history': 'Geçmiş',
|
||||
'clearHistory': 'Geçmişi Temizle',
|
||||
'noHistoryFound': 'Geçmiş Bulunamadı',
|
||||
@@ -126,7 +154,8 @@ const trTR = {
|
||||
'browserExtension': 'Tarayıcı Eklentisi',
|
||||
'launchAtStartup': 'Başlangıçta çalıştır',
|
||||
'runAsMenubarApp': 'Menü çubuğu uygulaması olarak çalıştır',
|
||||
'runAsMenubarAppDesc': 'Dock simgesini gizle ve sadece menü çubuğunda çalıştır',
|
||||
'runAsMenubarAppDesc':
|
||||
'Dock simgesini gizle ve sadece menü çubuğunda çalıştır',
|
||||
'seedConfig': 'Seed Yapılandırması',
|
||||
'seedKeep': 'Manuel olarak durdurulana kadar seed\'e devam et',
|
||||
'seedRatio': 'Seed oranı',
|
||||
@@ -158,7 +187,8 @@ const trTR = {
|
||||
'username_required': 'Lütfen kullanıcı adınızı girin',
|
||||
'password_required': 'Lütfen şifrenizi girin',
|
||||
'login_success': 'Giriş başarılı',
|
||||
'login_failed': 'Giriş başarısız, lütfen kullanıcı adınızı ve şifrenizi kontrol edin',
|
||||
'login_failed':
|
||||
'Giriş başarısız, lütfen kullanıcı adınızı ve şifrenizi kontrol edin',
|
||||
'login_failed_network':
|
||||
'Giriş başarısız, lütfen ağ bağlantınızı kontrol edin',
|
||||
'insertPlaceholder': 'Yer Tutucu Ekle',
|
||||
@@ -178,7 +208,8 @@ const trTR = {
|
||||
'selectCategory': 'Kategori Seç',
|
||||
'githubMirror': 'GitHub Yansısı',
|
||||
'githubMirrorEnable': 'GitHub Yansısını Etkinleştir',
|
||||
'githubMirrorDesc': 'GitHub içerik indirmelerini hızlandırmak için yansıları kullanın',
|
||||
'githubMirrorDesc':
|
||||
'GitHub içerik indirmelerini hızlandırmak için yansıları kullanın',
|
||||
'githubMirrorType': 'Yansı Türü',
|
||||
'githubMirrorUrl': 'Yansı URL\'si',
|
||||
'githubMirrorUrlHint': 'Yansı URL\'sini girin',
|
||||
@@ -189,10 +220,21 @@ const trTR = {
|
||||
'updateUrlCancelListen': 'İzlemeyi İptal Et',
|
||||
'updateUrlDialogHint': 'Yeni indirme URL\'sini girin',
|
||||
'pendingUpdateFound': 'Bekleyen Güncelleme İşlemi Bulundu',
|
||||
'pendingUpdateConfirm':
|
||||
'pendingUpdateConfirm':
|
||||
'"@name" işlemi URL güncellemesi bekliyor. Yeni URL ile güncellemek istiyor musunuz?',
|
||||
'pendingUpdateYes': 'İşlemi Güncelle',
|
||||
'pendingUpdateNo': 'Yeni İşlem Oluştur',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Masaüstü Bildirimleri',
|
||||
'notificationTaskDone': 'Görev tamamlandı',
|
||||
'notificationTaskError': 'Görev hatası',
|
||||
|
||||
@@ -152,6 +152,17 @@ const ukUA = {
|
||||
'categoryPath': 'Шлях категорії',
|
||||
'builtInCategory': 'Вбудовану категорію неможливо видалити',
|
||||
'selectCategory': 'Вибрати категорію',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Сповіщення',
|
||||
'notificationTaskDone': 'Завдання завершено',
|
||||
'notificationTaskError': 'Помилка завдання',
|
||||
|
||||
@@ -122,6 +122,17 @@ const viVN = {
|
||||
'launchAtStartup': 'Khởi động cùng hệ thống',
|
||||
'runAsMenubarApp': 'Chạy như ứng dụng thanh menu',
|
||||
'runAsMenubarAppDesc': 'Ẩn biểu tượng Dock và chỉ chạy trong thanh menu',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP Listen Port',
|
||||
'ed2kUdpPort': 'UDP Listen Port',
|
||||
'ed2kServerList': 'Server Addresses',
|
||||
'ed2kServerMet': 'Server.met Sources',
|
||||
'ed2kNodesDat': 'nodes.dat Sources',
|
||||
'ed2kAutoPort': 'Auto assign',
|
||||
'ed2kOnePerLine': 'One entry per line',
|
||||
'ed2kServersHint': 'host:port, one per line',
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Thông báo trên màn hình',
|
||||
'notificationTaskDone': 'Nhiệm vụ hoàn thành',
|
||||
'notificationTaskError': 'Lỗi nhiệm vụ',
|
||||
|
||||
@@ -209,6 +209,17 @@ const zhCN = {
|
||||
'pendingUpdateConfirm': '任务 "@name" 正在等待更新地址,是否使用新地址更新该任务?',
|
||||
'pendingUpdateYes': '更新任务',
|
||||
'pendingUpdateNo': '创建新任务',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP 监听端口',
|
||||
'ed2kUdpPort': 'UDP 监听端口',
|
||||
'ed2kServerList': '服务器地址',
|
||||
'ed2kServerMet': 'Server.met 源',
|
||||
'ed2kNodesDat': 'nodes.dat 源',
|
||||
'ed2kAutoPort': '自动分配',
|
||||
'ed2kOnePerLine': '每行填写一条',
|
||||
'ed2kServersHint': 'host:port,每行一条',
|
||||
'ed2kServerMetHint': '请输入 Server.met 地址或 ed2k 服务器列表链接,每行一条',
|
||||
'ed2kNodesDatHint': '请输入 nodes.dat 本地路径或 URL,每行一条',
|
||||
'desktopNotification': '桌面通知',
|
||||
'notificationTaskDone': '任务完成',
|
||||
'notificationTaskError': '任务失败',
|
||||
|
||||
@@ -147,6 +147,17 @@ const zhTW = {
|
||||
'categoryPath': '分類路徑',
|
||||
'builtInCategory': '內建分類無法刪除',
|
||||
'selectCategory': '選擇分類',
|
||||
'ed2k': 'ED2K',
|
||||
'ed2kTcpPort': 'TCP 監聽連接埠',
|
||||
'ed2kUdpPort': 'UDP 監聽連接埠',
|
||||
'ed2kServerList': '伺服器位址',
|
||||
'ed2kServerMet': 'Server.met 來源',
|
||||
'ed2kNodesDat': 'nodes.dat 來源',
|
||||
'ed2kAutoPort': '自動分配',
|
||||
'ed2kOnePerLine': '每行填寫一條',
|
||||
'ed2kServersHint': 'host:port,每行一條',
|
||||
'ed2kServerMetHint': '請輸入 Server.met 位址或 ed2k 伺服器清單連結,每行一條',
|
||||
'ed2kNodesDatHint': '請輸入 nodes.dat 本機路徑或 URL,每行一條',
|
||||
'desktopNotification': '桌面通知',
|
||||
'notificationTaskDone': '任務完成',
|
||||
'notificationTaskError': '任務失敗',
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'langs/es_es.dart';
|
||||
import 'langs/uk_ua.dart';
|
||||
import 'langs/hu_hu.dart';
|
||||
import 'langs/pt_br.dart';
|
||||
import 'langs/ca_es.dart';
|
||||
|
||||
final messages = _Messages();
|
||||
|
||||
@@ -42,6 +43,7 @@ class _Messages extends Translations {
|
||||
...ukUA,
|
||||
...huHU,
|
||||
...ptBR,
|
||||
...deDE
|
||||
...deDE,
|
||||
...caES
|
||||
};
|
||||
}
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.9.2+1
|
||||
version: 1.9.3+1
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
Reference in new issue
Block a user