feat: ed2k protocol support (#1316)

This commit is contained in:
Levi authored and GitHub committed 2026-03-17 19:42:16 +08:00
1 parent f7b26753c1
commit 09f7c88ed3
35 files changed
+1129 -35

No files matched your search

+1
View File
@@ -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
+4
View File
@@ -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=
+15
View File
@@ -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
+15
View File
@@ -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"`
}
+505
View File
@@ -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)
}()
}
}
+42
View File
@@ -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)
}
}
+47 -6
View File
@@ -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
}
@@ -1300,8 +1335,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
+10
View File
@@ -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"
@@ -110,6 +111,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 +165,7 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
cfg.FetchManagers = []fetcher.FetcherManager{
new(http.FetcherManager),
new(bt.FetcherManager),
new(ed2k.FetcherManager),
}
}
if cfg.RefreshInterval == 0 {
+18
View File
@@ -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)
}
}
+14
View File
@@ -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? ?? '',
+1 -1
View File
@@ -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 {
+1
View File
@@ -51,6 +51,7 @@ const _$StatusEnumMap = {
const _$ProtocolEnumMap = {
Protocol.http: 'http',
Protocol.bt: 'bt',
Protocol.ed2k: 'ed2k',
};
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
@@ -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(
+37 -13
View File
@@ -35,14 +35,18 @@ const caES = {
'downloadDir': 'Carpeta de baixades',
'downloadDirValid': 'Seleccioneu la carpeta de baixades',
'connections': 'Connexions',
'useServerCtime': 'Utilitza l\'hora del servidor per a la creació de fitxers',
'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',
'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',
'autoDeleteMissingFileTasks':
'Suprimeix automàticament les tasques dels fitxers que falten',
'items': '@count elements',
'subscribeTracker': 'Subscripció al rastrejador',
'subscribeFail':
@@ -51,7 +55,8 @@ const caES = {
'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',
'addTrackerHit':
'Introduïu la URL del servidor del rastrejador, un per línia',
'ui': 'UI',
'theme': 'Tema',
'themeSystem': 'Sistema',
@@ -60,7 +65,8 @@ const caES = {
'locale': 'Idioma',
'notifyWhenNewVersion': 'Notificació d\'actualitzacions',
'analyticsEnabled': 'Puja les estadístiques',
'analyticsEnabledDesc': 'Compartiu dades d\'ús anònimes per ajudar-nos a millorar',
'analyticsEnabledDesc':
'Compartiu dades d\'ús anònimes per ajudar-nos a millorar',
'about': 'Quant a',
'homepage': 'Pàgina d\'inici',
'version': 'Versió',
@@ -75,15 +81,18 @@ const caES = {
'logDirectory': 'Carpeta dels registres',
'webhook': 'Webhook',
'webhookEnable': 'Activa el Webhook',
'webhookDesc': 'Envia notificacions HTTP POST quan les tasques finalitzin o fallin',
'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)',
'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',
@@ -147,7 +156,8 @@ const caES = {
'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ú',
'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',
@@ -161,7 +171,8 @@ const caES = {
'archives': 'Comprimits',
'autoExtract': 'Extreu els comprimits automàticament',
'archivePassword': 'Contrasenya del comprimit',
'archivePasswordHint': 'Deixeu-lo en blanc si no està protegit amb contrasenya',
'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',
@@ -179,7 +190,8 @@ const caES = {
'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':
'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ó',
@@ -199,7 +211,8 @@ const caES = {
'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',
'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',
@@ -214,6 +227,17 @@ const caES = {
'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',
+11
View File
@@ -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',
+11
View File
@@ -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',
+11
View File
@@ -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',
+11
View File
@@ -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': 'خطای وظیفه',
+11
View File
@@ -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',
+11
View File
@@ -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',
+11
View File
@@ -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',
+11
View File
@@ -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à',
+11
View File
@@ -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': 'タスクエラー',
+11
View File
@@ -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',
+11
View File
@@ -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',
+11
View File
@@ -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': 'Ошибка задачи',
+11
View File
@@ -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': 'பணி பிழை',
+32 -11
View File
@@ -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',
@@ -147,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ı',
@@ -179,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',
@@ -199,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',
@@ -210,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ı',
+11
View File
@@ -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': 'Помилка завдання',
+11
View File
@@ -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ụ',
+11
View File
@@ -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': '任务失败',
+11
View File
@@ -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': '任務失敗',